Files
pack/core/app/app.go
T

220 lines
4.6 KiB
Go

package app
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"github.com/BurntSushi/toml"
"github.com/bmatcuk/doublestar/v4"
"github.com/tailscale/hujson"
"gopkg.in/yaml.v2"
)
type App struct {
Source string
}
func NewApp(path string) (*App, error) {
var source string
if filepath.IsAbs(path) {
source = path
} else {
currentDir, err := os.Getwd()
if err != nil {
return nil, err
}
source, err = filepath.Abs(filepath.Join(currentDir, path))
if err != nil {
return nil, errors.New("failed to read app source directory")
}
}
if _, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil, fmt.Errorf("directory %s does not exist", source)
}
return nil, fmt.Errorf("failed to check directory %s: %w", source, err)
}
return &App{Source: source}, nil
}
// findMatches returns a list of paths matching a glob pattern, filtered by isDir
func (a *App) findMatches(pattern string, isDir bool) ([]string, error) {
matches, err := a.findGlob(pattern)
if err != nil {
return nil, err
}
var paths []string
for _, match := range matches {
fullPath := filepath.Join(a.Source, match)
info, err := os.Stat(fullPath)
if err != nil {
continue
}
if info.IsDir() == isDir {
paths = append(paths, match)
}
}
return paths, nil
}
// FindFiles returns a list of file paths matching a glob pattern
func (a *App) FindFiles(pattern string) ([]string, error) {
return a.findMatches(pattern, false)
}
// FindDirectories returns a list of directory paths matching a glob pattern
func (a *App) FindDirectories(pattern string) ([]string, error) {
return a.findMatches(pattern, true)
}
// findGlob finds paths matching a glob pattern
func (a *App) findGlob(pattern string) ([]string, error) {
matches, err := doublestar.Glob(os.DirFS(a.Source), pattern)
if err != nil {
return nil, err
}
return matches, nil
}
// HasMatch checks if a path matching a glob exists (files or directories)
func (a *App) HasMatch(pattern string) bool {
files, err := a.FindFiles(pattern)
if err != nil {
return false
}
dirs, err := a.FindDirectories(pattern)
if err != nil {
return false
}
return len(files) > 0 || len(dirs) > 0
}
func (a *App) FindFilesWithContent(pattern string, regex *regexp.Regexp) []string {
files, err := a.FindFiles(pattern)
if err != nil {
return nil
}
var matches []string
for _, file := range files {
content, err := a.ReadFile(file)
if err != nil {
continue
}
if regex.MatchString(content) {
matches = append(matches, file)
}
}
return matches
}
// ReadFile reads the contents of a file
func (a *App) ReadFile(name string) (string, error) {
path := filepath.Join(a.Source, name)
data, err := os.ReadFile(path)
if err != nil {
relativePath, _ := a.stripSourcePath(path)
return "", fmt.Errorf("error reading %s: %w", relativePath, err)
}
return strings.ReplaceAll(string(data), "\r\n", "\n"), nil
}
// ReadJSON reads and parses a JSON file
func (a *App) ReadJSON(name string, v interface{}) error {
data, err := a.ReadFile(name)
if err != nil {
return err
}
jsonBytes, err := standardizeJSON([]byte(data))
if err != nil {
return err
}
data = string(jsonBytes)
if err := json.Unmarshal([]byte(data), v); err != nil {
relativePath, _ := a.stripSourcePath(filepath.Join(a.Source, name))
return fmt.Errorf("error reading %s as JSON: %w", relativePath, err)
}
return nil
}
// ReadYAML reads and parses a YAML file
func (a *App) ReadYAML(name string, v interface{}) error {
data, err := a.ReadFile(name)
if err != nil {
return err
}
if err := yaml.Unmarshal([]byte(data), v); err != nil {
return fmt.Errorf("error reading %s as YAML: %w", name, err)
}
return nil
}
// ReadTOML reads and parses a TOML file
func (a *App) ReadTOML(name string, v interface{}) error {
data, err := a.ReadFile(name)
if err != nil {
return err
}
return toml.Unmarshal([]byte(data), v)
}
// IsFileExecutable checks if a path is an executable file
func (a *App) IsFileExecutable(name string) bool {
path := filepath.Join(a.Source, name)
info, err := os.Stat(path)
if err != nil {
return false
}
if !info.Mode().IsRegular() {
return false
}
// Check executable bit
return info.Mode()&0111 != 0
}
// StripSourcePath converts an absolute path to a path relative to the app source directory
func (a *App) stripSourcePath(absPath string) (string, error) {
rel, err := filepath.Rel(a.Source, absPath)
if err != nil {
return "", errors.New("failed to parse source path")
}
return rel, nil
}
func standardizeJSON(b []byte) ([]byte, error) {
ast, err := hujson.Parse(b)
if err != nil {
return b, err
}
ast.Standardize()
return ast.Pack(), nil
}