Apply standard Go formatting and update project documentation
- Run go fmt on all Go files to ensure consistent formatting - Add official Go tooling commands to CLAUDE.md for code quality - Update project status to reflect current implementation state
This commit is contained in:
parent
1265f9fb07
commit
bde7aeed90
7 changed files with 104 additions and 72 deletions
32
CLAUDE.md
Normal file
32
CLAUDE.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a CLI tool called "collect" that recursively collects files matching specific criteria, maintains their file structure, and archives them for backup purposes.
|
||||
|
||||
## Project Status
|
||||
|
||||
The collect CLI tool has been implemented with the following features:
|
||||
- Multiple `--name` flags for exact filename matching
|
||||
- Multiple `--match` flags for directory pattern matching
|
||||
- Support for combining `--name` and `--match` flags with OR logic
|
||||
- Preserving original directory structure in the output
|
||||
- Creating archives in tar.gz or zip format
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
The tool accepts:
|
||||
1. Multiple `--name` and `--match` flags in any combination
|
||||
2. Two positional arguments: source directory and output archive path
|
||||
3. Archive format determined by output file extension (.tgz/.tar.gz or .zip)
|
||||
4. Recursive directory traversal while preserving original structure
|
||||
|
||||
## Code Quality
|
||||
|
||||
Before committing changes, run these official Go tools:
|
||||
- `go fmt ./...` - Format all Go files to standard style
|
||||
- `go vet ./...` - Check for common mistakes and suspicious code
|
||||
- `go test ./...` - Run unit tests (when available)
|
||||
- `./test.sh` - Run integration tests
|
|
@ -5,4 +5,4 @@ import "github.com/atomaka/collect/collector"
|
|||
// Archiver defines the interface for creating archives
|
||||
type Archiver interface {
|
||||
Create(outputPath string, files []collector.FileEntry) error
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
||||
"github.com/atomaka/collect/collector"
|
||||
)
|
||||
|
||||
|
@ -27,22 +27,22 @@ func (a *TarArchiver) Create(outputPath string, files []collector.FileEntry) err
|
|||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
|
||||
// Create gzip writer
|
||||
gzipWriter := gzip.NewWriter(outFile)
|
||||
defer gzipWriter.Close()
|
||||
|
||||
|
||||
// Create tar writer
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
defer tarWriter.Close()
|
||||
|
||||
|
||||
// Add each file to the archive
|
||||
for _, file := range files {
|
||||
if err := a.addFileToTar(tarWriter, file); err != nil {
|
||||
return fmt.Errorf("failed to add file %s: %w", file.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -59,28 +59,28 @@ func (a *TarArchiver) addFileToTar(tw *tar.Writer, file collector.FileEntry) err
|
|||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
|
||||
// Get file info
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Create tar header
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Use the relative path in the archive
|
||||
header.Name = filepath.ToSlash(file.Path)
|
||||
|
||||
|
||||
// Write header
|
||||
if err := tw.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Copy file contents
|
||||
_, err = io.Copy(tw, f)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
|
||||
"github.com/atomaka/collect/collector"
|
||||
)
|
||||
|
||||
|
@ -26,18 +26,18 @@ func (a *ZipArchiver) Create(outputPath string, files []collector.FileEntry) err
|
|||
return fmt.Errorf("failed to create output file: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
|
||||
// Create zip writer
|
||||
zipWriter := zip.NewWriter(outFile)
|
||||
defer zipWriter.Close()
|
||||
|
||||
|
||||
// Add each file to the archive
|
||||
for _, file := range files {
|
||||
if err := a.addFileToZip(zipWriter, file); err != nil {
|
||||
return fmt.Errorf("failed to add file %s: %w", file.Path, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -54,32 +54,32 @@ func (a *ZipArchiver) addFileToZip(zw *zip.Writer, file collector.FileEntry) err
|
|||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
|
||||
// Get file info
|
||||
info, err := f.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Create zip file header
|
||||
header, err := zip.FileInfoHeader(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Use the relative path in the archive
|
||||
header.Name = filepath.ToSlash(file.Path)
|
||||
|
||||
|
||||
// Set compression method
|
||||
header.Method = zip.Deflate
|
||||
|
||||
|
||||
// Create writer for this file
|
||||
writer, err := zw.CreateHeader(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Copy file contents
|
||||
_, err = io.Copy(writer, f)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) {
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get absolute path: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Check if source directory exists
|
||||
info, err := os.Stat(absSourceDir)
|
||||
if err != nil {
|
||||
|
@ -42,9 +42,9 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) {
|
|||
if !info.IsDir() {
|
||||
return nil, fmt.Errorf("source path is not a directory: %s", sourceDir)
|
||||
}
|
||||
|
||||
|
||||
var files []FileEntry
|
||||
|
||||
|
||||
err = filepath.WalkDir(absSourceDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
// Log permission errors but continue walking
|
||||
|
@ -54,7 +54,7 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Get file info
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
|
@ -64,12 +64,12 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) {
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
// Skip symlinks
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
// Check if this file should be included
|
||||
if c.matcher.ShouldInclude(path, info) {
|
||||
// Calculate relative path from source directory
|
||||
|
@ -77,42 +77,42 @@ func (c *Collector) Collect(sourceDir string) ([]FileEntry, error) {
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to get relative path: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Clean the relative path to ensure consistent formatting
|
||||
relPath = filepath.ToSlash(relPath)
|
||||
|
||||
|
||||
files = append(files, FileEntry{
|
||||
Path: relPath,
|
||||
FullPath: path,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("walk error: %w", err)
|
||||
}
|
||||
|
||||
|
||||
// Check if any files were found
|
||||
if len(files) == 0 {
|
||||
return nil, fmt.Errorf("no files found matching criteria")
|
||||
}
|
||||
|
||||
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// GetArchiveFormat determines the archive format from the filename
|
||||
func GetArchiveFormat(filename string) string {
|
||||
lower := strings.ToLower(filename)
|
||||
|
||||
|
||||
if strings.HasSuffix(lower, ".tar.gz") || strings.HasSuffix(lower, ".tgz") {
|
||||
return "tar.gz"
|
||||
}
|
||||
|
||||
|
||||
if strings.HasSuffix(lower, ".zip") {
|
||||
return "zip"
|
||||
}
|
||||
|
||||
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ type PatternMatcher struct {
|
|||
func NewPatternMatcher(pattern string) *PatternMatcher {
|
||||
// Remove trailing slash if present
|
||||
pattern = strings.TrimSuffix(pattern, "/")
|
||||
|
||||
|
||||
return &PatternMatcher{
|
||||
pattern: pattern,
|
||||
matchedDirs: make(map[string]bool),
|
||||
|
@ -58,27 +58,27 @@ func (m *PatternMatcher) ShouldInclude(path string, info os.FileInfo) bool {
|
|||
}
|
||||
return false // Don't include the directory itself, only files within
|
||||
}
|
||||
|
||||
|
||||
// For files, check if any parent directory is in the matched set
|
||||
dir := filepath.Dir(path)
|
||||
for {
|
||||
if m.matchedDirs[dir] {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
// Also check if this directory matches the pattern (in case we haven't seen it yet)
|
||||
if matched, err := m.dirMatchesPattern(dir); err == nil && matched {
|
||||
m.matchedDirs[dir] = true
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
if parent == dir || parent == "." {
|
||||
break
|
||||
}
|
||||
dir = parent
|
||||
}
|
||||
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -86,26 +86,26 @@ func (m *PatternMatcher) ShouldInclude(path string, info os.FileInfo) bool {
|
|||
func (m *PatternMatcher) dirMatchesPattern(dirPath string) (bool, error) {
|
||||
// Get the directory name
|
||||
dirName := filepath.Base(dirPath)
|
||||
|
||||
|
||||
// For simple patterns (no path separators), just match the directory name
|
||||
if len(m.patternSegments) == 1 {
|
||||
return filepath.Match(m.pattern, dirName)
|
||||
}
|
||||
|
||||
|
||||
// For complex patterns, we need to match the full path segments
|
||||
pathSegments := strings.Split(dirPath, string(os.PathSeparator))
|
||||
|
||||
|
||||
// Try to match the pattern segments against the path segments
|
||||
if len(pathSegments) < len(m.patternSegments) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
||||
// Check each pattern segment against the corresponding path segment
|
||||
for i := 0; i < len(m.patternSegments); i++ {
|
||||
// Start from the end of both slices
|
||||
patternIdx := len(m.patternSegments) - 1 - i
|
||||
pathIdx := len(pathSegments) - 1 - i
|
||||
|
||||
|
||||
matched, err := filepath.Match(m.patternSegments[patternIdx], pathSegments[pathIdx])
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
@ -114,7 +114,7 @@ func (m *PatternMatcher) dirMatchesPattern(dirPath string) (bool, error) {
|
|||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
|
@ -136,4 +136,4 @@ func (m *CompositeMatcher) ShouldInclude(path string, info os.FileInfo) bool {
|
|||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
42
main.go
42
main.go
|
@ -6,16 +6,16 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
|
||||
"github.com/atomaka/collect/archiver"
|
||||
"github.com/atomaka/collect/collector"
|
||||
)
|
||||
|
||||
const (
|
||||
exitSuccess = 0
|
||||
exitNoFiles = 1
|
||||
exitSuccess = 0
|
||||
exitNoFiles = 1
|
||||
exitArchiveError = 2
|
||||
exitInvalidArgs = 3
|
||||
exitInvalidArgs = 3
|
||||
)
|
||||
|
||||
// stringSlice is a custom flag type that accumulates string values
|
||||
|
@ -34,10 +34,10 @@ func main() {
|
|||
// Define flags using custom type for multiple values
|
||||
var nameFlags stringSlice
|
||||
var matchFlags stringSlice
|
||||
|
||||
|
||||
flag.Var(&nameFlags, "name", "Match exact filename (can be specified multiple times)")
|
||||
flag.Var(&matchFlags, "match", "Match directory pattern (can be specified multiple times)")
|
||||
|
||||
|
||||
// Custom usage message
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, "Usage: %s [--name <filename>]... [--match <pattern>]... <source-dir> <output-archive>\n\n", os.Args[0])
|
||||
|
@ -49,16 +49,16 @@ func main() {
|
|||
fmt.Fprintf(os.Stderr, " %s --match 'aet-*/' ./ backup.zip\n", os.Args[0])
|
||||
fmt.Fprintf(os.Stderr, " %s --name .mise.toml --name README.md --match 'test-*' ./ backup.tgz\n", os.Args[0])
|
||||
}
|
||||
|
||||
|
||||
flag.Parse()
|
||||
|
||||
|
||||
// Validate flags
|
||||
if len(nameFlags) == 0 && len(matchFlags) == 0 {
|
||||
fmt.Fprintf(os.Stderr, "Error: At least one --name or --match must be specified\n\n")
|
||||
flag.Usage()
|
||||
os.Exit(exitInvalidArgs)
|
||||
}
|
||||
|
||||
|
||||
// Check positional arguments
|
||||
args := flag.Args()
|
||||
if len(args) != 2 {
|
||||
|
@ -66,30 +66,30 @@ func main() {
|
|||
flag.Usage()
|
||||
os.Exit(exitInvalidArgs)
|
||||
}
|
||||
|
||||
|
||||
sourceDir := args[0]
|
||||
outputPath := args[1]
|
||||
|
||||
|
||||
// Determine archive format
|
||||
format := collector.GetArchiveFormat(outputPath)
|
||||
if format == "" {
|
||||
fmt.Fprintf(os.Stderr, "Error: Unsupported archive format. Use .tar.gz, .tgz, or .zip\n")
|
||||
os.Exit(exitInvalidArgs)
|
||||
}
|
||||
|
||||
|
||||
// Create matchers
|
||||
var matchers []collector.Matcher
|
||||
|
||||
|
||||
// Add name matchers
|
||||
for _, name := range nameFlags {
|
||||
matchers = append(matchers, collector.NewNameMatcher(name))
|
||||
}
|
||||
|
||||
|
||||
// Add pattern matchers
|
||||
for _, pattern := range matchFlags {
|
||||
matchers = append(matchers, collector.NewPatternMatcher(pattern))
|
||||
}
|
||||
|
||||
|
||||
// Create a composite matcher if we have multiple matchers, otherwise use the single one
|
||||
var matcher collector.Matcher
|
||||
if len(matchers) == 1 {
|
||||
|
@ -97,7 +97,7 @@ func main() {
|
|||
} else {
|
||||
matcher = collector.NewCompositeMatcher(matchers)
|
||||
}
|
||||
|
||||
|
||||
// Create collector and collect files
|
||||
c := collector.New(matcher)
|
||||
files, err := c.Collect(sourceDir)
|
||||
|
@ -108,10 +108,10 @@ func main() {
|
|||
}
|
||||
os.Exit(exitArchiveError)
|
||||
}
|
||||
|
||||
|
||||
// Report number of files found
|
||||
fmt.Printf("Found %d files to archive\n", len(files))
|
||||
|
||||
|
||||
// Create appropriate archiver
|
||||
var arch archiver.Archiver
|
||||
switch format {
|
||||
|
@ -120,14 +120,14 @@ func main() {
|
|||
case "zip":
|
||||
arch = archiver.NewZipArchiver()
|
||||
}
|
||||
|
||||
|
||||
// Create archive
|
||||
if err := arch.Create(outputPath, files); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error creating archive: %v\n", err)
|
||||
os.Exit(exitArchiveError)
|
||||
}
|
||||
|
||||
|
||||
// Get absolute path for cleaner output
|
||||
absOutput, _ := filepath.Abs(outputPath)
|
||||
fmt.Printf("Archive created successfully: %s\n", absOutput)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue