Skip to content

Commit d6ef585

Browse files
author
Spencer Brower
committed
refactor: Rewrote in golang.
1 parent e38f8a4 commit d6ef585

27 files changed

+1945
-343
lines changed

.envrc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
11
use flake
2-
3-
export PATH="$PATH:./vendor/bin"

.gitignore

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
# dev env
12
.direnv
2-
node_modules
3-
vendor
3+
4+
# docs
5+
man
6+
7+
# build artifacts
8+
envr
9+
result

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
Have you ever wanted to back up all your .env files in case your hard drive gets
44
nuked? `envr` makes it easier.
55

6-
`envr` is a [Nushell](https://www.nushell.sh) script that tracks your `.env` files
6+
`envr` is a binary applicate that tracks your `.env` files
77
in an encyrpted sqlite database. Changes can be effortlessly synced with
88
`envr sync`, and restored with `envr restore`.
99

app/config.go

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
package app
2+
3+
import (
4+
"encoding/json"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"strings"
11+
12+
"filippo.io/age"
13+
"filippo.io/age/agessh"
14+
)
15+
16+
type Config struct {
17+
Keys []SshKeyPair `json:"keys"`
18+
ScanConfig scanConfig `json:"scan"`
19+
}
20+
21+
type SshKeyPair struct {
22+
Private string `json:"private"` // Path to the private key file
23+
Public string `json:"public"` // Path to the public key file
24+
}
25+
26+
type scanConfig struct {
27+
Matcher string `json:"matcher"`
28+
Exclude string `json:"exclude"`
29+
Include string `json:"include"`
30+
}
31+
32+
// Create a fresh config with sensible defaults.
33+
func NewConfig(privateKeyPaths []string) Config {
34+
var keys = []SshKeyPair{}
35+
36+
for _, priv := range privateKeyPaths {
37+
var key = SshKeyPair{
38+
Private: priv,
39+
Public: priv + ".pub",
40+
}
41+
42+
keys = append(keys, key)
43+
}
44+
45+
return Config{
46+
Keys: keys,
47+
ScanConfig: scanConfig{
48+
Matcher: "\\.env",
49+
Exclude: "*.envrc",
50+
Include: "~",
51+
},
52+
}
53+
}
54+
55+
// Read the Config from disk.
56+
func LoadConfig() (*Config, error) {
57+
homeDir, err := os.UserHomeDir()
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
configPath := filepath.Join(homeDir, ".envr", "config.json")
63+
64+
data, err := os.ReadFile(configPath)
65+
if err != nil {
66+
if errors.Is(err, os.ErrNotExist) {
67+
return nil, fmt.Errorf("No config file found. Please run `envr init` to generate one.")
68+
} else {
69+
return nil, err
70+
}
71+
}
72+
73+
var config Config
74+
if err := json.Unmarshal(data, &config); err != nil {
75+
return nil, err
76+
}
77+
78+
return &config, nil
79+
}
80+
81+
// Write the Config to disk.
82+
func (c *Config) Save() error {
83+
// Create the ~/.envr directory
84+
homeDir, err := os.UserHomeDir()
85+
if err != nil {
86+
return err
87+
}
88+
configDir := filepath.Join(homeDir, ".envr")
89+
if err := os.MkdirAll(configDir, 0755); err != nil {
90+
return err
91+
}
92+
93+
configPath := filepath.Join(configDir, "config.json")
94+
95+
// Check if file exists and is not empty
96+
if info, err := os.Stat(configPath); err == nil {
97+
if info.Size() > 0 {
98+
return os.ErrExist
99+
}
100+
}
101+
102+
data, err := json.MarshalIndent(c, "", " ")
103+
if err != nil {
104+
return err
105+
}
106+
107+
return os.WriteFile(configPath, data, 0644)
108+
}
109+
110+
// Use fd to find all ignored .env files that match the config's parameters
111+
func (c Config) scan() (paths []string, err error) {
112+
searchPath, err := c.searchPath()
113+
if err != nil {
114+
return []string{}, err
115+
}
116+
117+
// Find all files (including ignored ones)
118+
fmt.Printf("Searching for all files in \"%s\"...\n", searchPath)
119+
allCmd := exec.Command("fd", "-a", c.ScanConfig.Matcher, "-E", c.ScanConfig.Exclude, "-HI", searchPath)
120+
allOutput, err := allCmd.Output()
121+
if err != nil {
122+
return []string{}, err
123+
}
124+
125+
allFiles := strings.Split(strings.TrimSpace(string(allOutput)), "\n")
126+
if len(allFiles) == 1 && allFiles[0] == "" {
127+
allFiles = []string{}
128+
}
129+
130+
// Find unignored files
131+
fmt.Printf("Search for unignored fies in \"%s\"...\n", searchPath)
132+
unignoredCmd := exec.Command("fd", "-a", c.ScanConfig.Matcher, "-E", c.ScanConfig.Exclude, "-H", searchPath)
133+
unignoredOutput, err := unignoredCmd.Output()
134+
if err != nil {
135+
return []string{}, err
136+
}
137+
138+
unignoredFiles := strings.Split(strings.TrimSpace(string(unignoredOutput)), "\n")
139+
if len(unignoredFiles) == 1 && unignoredFiles[0] == "" {
140+
unignoredFiles = []string{}
141+
}
142+
143+
// Create a map for faster lookup
144+
unignoredMap := make(map[string]bool)
145+
for _, file := range unignoredFiles {
146+
unignoredMap[file] = true
147+
}
148+
149+
// Filter to get only ignored files
150+
var ignoredFiles []string
151+
for _, file := range allFiles {
152+
if !unignoredMap[file] {
153+
ignoredFiles = append(ignoredFiles, file)
154+
}
155+
}
156+
157+
return ignoredFiles, nil
158+
}
159+
160+
func (c Config) searchPath() (path string, err error) {
161+
include := c.ScanConfig.Include
162+
163+
if include == "~" {
164+
homeDir, err := os.UserHomeDir()
165+
if err != nil {
166+
return "", err
167+
}
168+
return homeDir, nil
169+
}
170+
171+
absPath, err := filepath.Abs(include)
172+
if err != nil {
173+
return "", err
174+
}
175+
176+
return absPath, nil
177+
}
178+
179+
// TODO: Should this be private?
180+
func (s SshKeyPair) Identity() (age.Identity, error) {
181+
sshKey, err := os.ReadFile(s.Private)
182+
if err != nil {
183+
return nil, fmt.Errorf("failed to read SSH key: %w", err)
184+
}
185+
186+
id, err := agessh.ParseIdentity(sshKey)
187+
if err != nil {
188+
return nil, fmt.Errorf("failed to parse SSH identity: %w", err)
189+
}
190+
191+
return id, nil
192+
}
193+
194+
// TODO: Should this be private?
195+
func (s SshKeyPair) Recipient() (age.Recipient, error) {
196+
sshKey, err := os.ReadFile(s.Public)
197+
if err != nil {
198+
return nil, fmt.Errorf("failed to read SSH key: %w", err)
199+
}
200+
201+
id, err := agessh.ParseRecipient(string(sshKey))
202+
if err != nil {
203+
return nil, fmt.Errorf("failed to parse SSH identity: %w", err)
204+
}
205+
206+
return id, nil
207+
}

0 commit comments

Comments
 (0)