165 lines
4.0 KiB
Go
165 lines
4.0 KiB
Go
package extension
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/rogeecn/renamer/internal/traversal"
|
|
)
|
|
|
|
// PlannedRename describes a filesystem rename operation produced during planning.
|
|
type PlannedRename struct {
|
|
OriginalRelative string
|
|
OriginalAbsolute string
|
|
ProposedRelative string
|
|
ProposedAbsolute string
|
|
SourceExtension string
|
|
IsDir bool
|
|
Depth int
|
|
}
|
|
|
|
// PlanResult captures the preview summary and concrete operations required for apply.
|
|
type PlanResult struct {
|
|
Summary *ExtensionSummary
|
|
Operations []PlannedRename
|
|
}
|
|
|
|
// BuildPlan walks the scoped filesystem, collecting preview entries and rename operations.
|
|
func BuildPlan(ctx context.Context, req *ExtensionRequest) (*PlanResult, error) {
|
|
if req == nil {
|
|
return nil, errors.New("extension request cannot be nil")
|
|
}
|
|
if err := req.Normalize(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
summary := NewSummary()
|
|
operations := make([]PlannedRename, 0)
|
|
detector := newConflictDetector()
|
|
|
|
targetExt := NormalizeTargetExtension(req.TargetExtension)
|
|
targetCanonical := CanonicalExtension(targetExt)
|
|
|
|
sourceSet := make(map[string]struct{}, len(req.SourceExtensions))
|
|
for _, source := range req.SourceExtensions {
|
|
sourceSet[CanonicalExtension(source)] = struct{}{}
|
|
}
|
|
|
|
// Include the target extension so preview surfaces existing matches as no-ops.
|
|
matchable := make(map[string]struct{}, len(sourceSet)+1)
|
|
for ext := range sourceSet {
|
|
matchable[ext] = struct{}{}
|
|
}
|
|
matchable[targetCanonical] = struct{}{}
|
|
|
|
filterSet := make(map[string]struct{}, len(req.ExtensionFilter))
|
|
for _, filter := range req.ExtensionFilter {
|
|
filterSet[CanonicalExtension(filter)] = struct{}{}
|
|
}
|
|
|
|
walker := traversal.NewWalker()
|
|
|
|
err := walker.Walk(
|
|
req.WorkingDir,
|
|
req.Recursive,
|
|
req.IncludeDirs,
|
|
req.IncludeHidden,
|
|
0,
|
|
func(relPath string, entry fs.DirEntry, depth int) error {
|
|
select {
|
|
case <-ctx.Done():
|
|
return ctx.Err()
|
|
default:
|
|
}
|
|
|
|
if relPath == "." {
|
|
return nil
|
|
}
|
|
|
|
isDir := entry.IsDir()
|
|
if isDir && !req.IncludeDirs {
|
|
return nil
|
|
}
|
|
|
|
name := entry.Name()
|
|
rawExt := strings.TrimSpace(filepath.Ext(name))
|
|
canonicalExt := CanonicalExtension(rawExt)
|
|
|
|
if !isDir && len(filterSet) > 0 {
|
|
if _, ok := filterSet[canonicalExt]; !ok {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if _, ok := matchable[canonicalExt]; !ok {
|
|
return nil
|
|
}
|
|
|
|
relative := filepath.ToSlash(relPath)
|
|
originalAbsolute := filepath.Join(req.WorkingDir, filepath.FromSlash(relative))
|
|
|
|
status := PreviewStatusChanged
|
|
targetRelative := relative
|
|
targetAbsolute := originalAbsolute
|
|
|
|
if canonicalExt == targetCanonical && rawExt == targetExt {
|
|
status = PreviewStatusNoChange
|
|
}
|
|
|
|
if status == PreviewStatusChanged {
|
|
base := strings.TrimSuffix(name, rawExt)
|
|
targetName := base + targetExt
|
|
dir := filepath.Dir(relative)
|
|
if dir == "." {
|
|
dir = ""
|
|
}
|
|
if dir == "" {
|
|
targetRelative = filepath.ToSlash(targetName)
|
|
} else {
|
|
targetRelative = filepath.ToSlash(filepath.Join(dir, targetName))
|
|
}
|
|
targetAbsolute = filepath.Join(req.WorkingDir, filepath.FromSlash(targetRelative))
|
|
|
|
allowed, err := detector.evaluateTarget(summary, relative, targetRelative, originalAbsolute, targetAbsolute)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if allowed {
|
|
operations = append(operations, PlannedRename{
|
|
OriginalRelative: relative,
|
|
OriginalAbsolute: originalAbsolute,
|
|
ProposedRelative: targetRelative,
|
|
ProposedAbsolute: targetAbsolute,
|
|
SourceExtension: rawExt,
|
|
IsDir: isDir,
|
|
Depth: depth,
|
|
})
|
|
} else {
|
|
status = PreviewStatusSkipped
|
|
}
|
|
}
|
|
|
|
entrySummary := PreviewEntry{
|
|
OriginalPath: relative,
|
|
ProposedPath: targetRelative,
|
|
Status: status,
|
|
SourceExtension: rawExt,
|
|
}
|
|
summary.RecordEntry(entrySummary)
|
|
|
|
return nil
|
|
},
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &PlanResult{
|
|
Summary: summary,
|
|
Operations: operations,
|
|
}, nil
|
|
}
|