mirror of
https://github.com/sbrow/envr.git
synced 2025-12-29 23:47:39 -05:00
238 lines
5.2 KiB
Go
238 lines
5.2 KiB
Go
package app
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"filippo.io/age"
|
|
"filippo.io/age/agessh"
|
|
)
|
|
|
|
type Config struct {
|
|
Keys []SshKeyPair `json:"keys"`
|
|
ScanConfig scanConfig `json:"scan"`
|
|
}
|
|
|
|
// Used by age to encrypt and decrypt the database.
|
|
type SshKeyPair struct {
|
|
Private string `json:"private"` // Path to the private key file
|
|
Public string `json:"public"` // Path to the public key file
|
|
}
|
|
|
|
type scanConfig struct {
|
|
// TODO: Support multiple matchers
|
|
Matcher string `json:"matcher"`
|
|
Exclude []string `json:"exclude"`
|
|
Include []string `json:"include"`
|
|
}
|
|
|
|
// Create a fresh config with sensible defaults.
|
|
func NewConfig(privateKeyPaths []string) Config {
|
|
var keys = []SshKeyPair{}
|
|
|
|
for _, priv := range privateKeyPaths {
|
|
var key = SshKeyPair{
|
|
Private: priv,
|
|
Public: priv + ".pub",
|
|
}
|
|
|
|
keys = append(keys, key)
|
|
}
|
|
|
|
return Config{
|
|
Keys: keys,
|
|
ScanConfig: scanConfig{
|
|
Matcher: "\\.env",
|
|
Exclude: []string{
|
|
"*\\.envrc",
|
|
"\\.local/",
|
|
"node_modules",
|
|
"vendor",
|
|
},
|
|
Include: []string{"~"},
|
|
},
|
|
}
|
|
}
|
|
|
|
// Read the Config from disk.
|
|
func LoadConfig() (*Config, error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
configPath := filepath.Join(homeDir, ".envr", "config.json")
|
|
|
|
data, err := os.ReadFile(configPath)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return nil, fmt.Errorf("No config file found. Please run `envr init` to generate one.")
|
|
} else {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
var config Config
|
|
if err := json.Unmarshal(data, &config); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &config, nil
|
|
}
|
|
|
|
// Write the Config to disk.
|
|
func (c *Config) Save() error {
|
|
// Create the ~/.envr directory
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
configDir := filepath.Join(homeDir, ".envr")
|
|
if err := os.MkdirAll(configDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
|
|
configPath := filepath.Join(configDir, "config.json")
|
|
|
|
// Check if file exists and is not empty
|
|
if info, err := os.Stat(configPath); err == nil {
|
|
if info.Size() > 0 {
|
|
return os.ErrExist
|
|
}
|
|
}
|
|
|
|
data, err := json.MarshalIndent(c, "", " ")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return os.WriteFile(configPath, data, 0644)
|
|
}
|
|
|
|
// buildFdArgs builds the fd command arguments with multiple exclude patterns
|
|
func (c Config) buildFdArgs(searchPath string, includeIgnored bool) []string {
|
|
args := []string{"-a", c.ScanConfig.Matcher}
|
|
|
|
// Add exclude patterns
|
|
for _, exclude := range c.ScanConfig.Exclude {
|
|
args = append(args, "-E", exclude)
|
|
}
|
|
|
|
if includeIgnored {
|
|
args = append(args, "-HI")
|
|
} else {
|
|
args = append(args, "-H")
|
|
}
|
|
|
|
args = append(args, searchPath)
|
|
return args
|
|
}
|
|
|
|
// Use fd to find all ignored .env files that match the config's parameters
|
|
func (c Config) scan() (paths []string, err error) {
|
|
searchPaths, err := c.searchPaths()
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
|
|
for _, searchPath := range searchPaths {
|
|
// Find all files (including ignored ones)
|
|
fmt.Printf("Searching for all files in \"%s\"...\n", searchPath)
|
|
allCmd := exec.Command("fd", c.buildFdArgs(searchPath, true)...)
|
|
allOutput, err := allCmd.Output()
|
|
if err != nil {
|
|
return paths, err
|
|
}
|
|
|
|
allFiles := strings.Split(strings.TrimSpace(string(allOutput)), "\n")
|
|
if len(allFiles) == 1 && allFiles[0] == "" {
|
|
allFiles = []string{}
|
|
}
|
|
|
|
// Find unignored files
|
|
fmt.Printf("Search for unignored fies in \"%s\"...\n", searchPath)
|
|
unignoredCmd := exec.Command("fd", c.buildFdArgs(searchPath, false)...)
|
|
unignoredOutput, err := unignoredCmd.Output()
|
|
if err != nil {
|
|
return []string{}, err
|
|
}
|
|
|
|
unignoredFiles := strings.Split(strings.TrimSpace(string(unignoredOutput)), "\n")
|
|
if len(unignoredFiles) == 1 && unignoredFiles[0] == "" {
|
|
unignoredFiles = []string{}
|
|
}
|
|
|
|
// Create a map for faster lookup
|
|
unignoredMap := make(map[string]bool)
|
|
for _, file := range unignoredFiles {
|
|
unignoredMap[file] = true
|
|
}
|
|
|
|
// Filter to get only ignored files
|
|
var ignoredFiles []string
|
|
for _, file := range allFiles {
|
|
if !unignoredMap[file] {
|
|
ignoredFiles = append(ignoredFiles, file)
|
|
}
|
|
}
|
|
|
|
paths = append(paths, ignoredFiles...)
|
|
}
|
|
|
|
return paths, nil
|
|
}
|
|
|
|
func (c Config) searchPaths() (paths []string, err error) {
|
|
homeDir, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return paths, err
|
|
}
|
|
|
|
includes := c.ScanConfig.Include
|
|
|
|
for _, include := range includes {
|
|
path := strings.Replace(include, "~", homeDir, 1)
|
|
absPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
return paths, err
|
|
}
|
|
|
|
paths = append(paths, absPath)
|
|
}
|
|
|
|
return paths, nil
|
|
}
|
|
|
|
func (s SshKeyPair) identity() (age.Identity, error) {
|
|
sshKey, err := os.ReadFile(s.Private)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read SSH key: %w", err)
|
|
}
|
|
|
|
id, err := agessh.ParseIdentity(sshKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse SSH identity: %w", err)
|
|
}
|
|
|
|
return id, nil
|
|
}
|
|
|
|
func (s SshKeyPair) recipient() (age.Recipient, error) {
|
|
sshKey, err := os.ReadFile(s.Public)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to read SSH key: %w", err)
|
|
}
|
|
|
|
id, err := agessh.ParseRecipient(string(sshKey))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse SSH identity: %w", err)
|
|
}
|
|
|
|
return id, nil
|
|
}
|