feat(add): implement recursive file addition with progress tracking

This commit is contained in:
Yar Kravtsov
2025-07-29 08:47:14 +03:00
parent 36d76c881c
commit a6852e5ad5
4 changed files with 647 additions and 8 deletions

View File

@@ -827,3 +827,263 @@ func (l *Lnk) RunBootstrapScript(scriptName string) error {
return nil
}
// walkDirectory walks through a directory and returns all regular files
func (l *Lnk) walkDirectory(dirPath string) ([]string, error) {
var files []string
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Skip directories - we only want files
if info.IsDir() {
return nil
}
// Handle symlinks: include them as files if they point to regular files
if info.Mode()&os.ModeSymlink != 0 {
// For symlinks, we'll include them but the AddMultiple logic
// will handle validation appropriately
files = append(files, path)
return nil
}
// Include regular files
if info.Mode().IsRegular() {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, fmt.Errorf("failed to walk directory %s: %w", dirPath, err)
}
return files, nil
}
// ProgressCallback defines the signature for progress reporting callbacks
type ProgressCallback func(current, total int, currentFile string)
// AddRecursiveWithProgress adds directory contents individually with progress reporting
func (l *Lnk) AddRecursiveWithProgress(paths []string, progress ProgressCallback) error {
var allFiles []string
for _, path := range paths {
// Get absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("failed to get absolute path for %s: %w", path, err)
}
// Check if it's a directory
info, err := os.Stat(absPath)
if err != nil {
return fmt.Errorf("failed to stat %s: %w", path, err)
}
if info.IsDir() {
// Walk directory to get all files
files, err := l.walkDirectory(absPath)
if err != nil {
return fmt.Errorf("failed to walk directory %s: %w", path, err)
}
allFiles = append(allFiles, files...)
} else {
// It's a regular file, add it directly
allFiles = append(allFiles, absPath)
}
}
// Use AddMultiple for batch processing
if len(allFiles) == 0 {
return fmt.Errorf("no files found to add")
}
// Apply progress threshold: only show progress for >10 files
const progressThreshold = 10
if len(allFiles) > progressThreshold && progress != nil {
return l.addMultipleWithProgress(allFiles, progress)
}
// For small operations, use regular AddMultiple without progress
return l.AddMultiple(allFiles)
}
// addMultipleWithProgress adds multiple files with progress reporting
func (l *Lnk) addMultipleWithProgress(paths []string, progress ProgressCallback) error {
if len(paths) == 0 {
return nil
}
// Phase 1: Validate all paths first (same as AddMultiple)
var relativePaths []string
var absolutePaths []string
var infos []os.FileInfo
for _, filePath := range paths {
// Validate the file or directory
if err := l.fs.ValidateFileForAdd(filePath); err != nil {
return fmt.Errorf("validation failed for %s: %w", filePath, err)
}
// Get absolute path
absPath, err := filepath.Abs(filePath)
if err != nil {
return fmt.Errorf("failed to get absolute path for %s: %w", filePath, err)
}
// Get relative path for tracking
relativePath, err := getRelativePath(absPath)
if err != nil {
return fmt.Errorf("failed to get relative path for %s: %w", filePath, err)
}
// Check if this relative path is already managed
managedItems, err := l.getManagedItems()
if err != nil {
return fmt.Errorf("failed to get managed items: %w", err)
}
for _, item := range managedItems {
if item == relativePath {
return fmt.Errorf("❌ File is already managed by lnk: \033[31m%s\033[0m", relativePath)
}
}
// Get file info
info, err := os.Stat(absPath)
if err != nil {
return fmt.Errorf("failed to stat path %s: %w", filePath, err)
}
relativePaths = append(relativePaths, relativePath)
absolutePaths = append(absolutePaths, absPath)
infos = append(infos, info)
}
// Phase 2: Process all files with progress reporting
var rollbackActions []func() error
total := len(absolutePaths)
for i, absPath := range absolutePaths {
// Report progress
if progress != nil {
progress(i+1, total, filepath.Base(absPath))
}
relativePath := relativePaths[i]
info := infos[i]
// Generate repository path from relative path
storagePath := l.getHostStoragePath()
destPath := filepath.Join(storagePath, relativePath)
// Ensure destination directory exists
destDir := filepath.Dir(destPath)
if err := os.MkdirAll(destDir, 0755); err != nil {
// Rollback previous operations
l.rollbackOperations(rollbackActions)
return fmt.Errorf("failed to create destination directory: %w", err)
}
// Move to repository
if err := l.fs.Move(absPath, destPath, info); err != nil {
// Rollback previous operations
l.rollbackOperations(rollbackActions)
return fmt.Errorf("failed to move %s: %w", absPath, err)
}
// Create symlink
if err := l.fs.CreateSymlink(destPath, absPath); err != nil {
// Try to restore the file we just moved, then rollback others
_ = l.fs.Move(destPath, absPath, info)
l.rollbackOperations(rollbackActions)
return fmt.Errorf("failed to create symlink for %s: %w", absPath, err)
}
// Add to tracking
if err := l.addManagedItem(relativePath); err != nil {
// Restore this file and rollback others
_ = os.Remove(absPath)
_ = l.fs.Move(destPath, absPath, info)
l.rollbackOperations(rollbackActions)
return fmt.Errorf("failed to update tracking file for %s: %w", absPath, err)
}
// Add rollback action for this file
rollbackAction := l.createRollbackAction(absPath, destPath, relativePath, info)
rollbackActions = append(rollbackActions, rollbackAction)
}
// Phase 3: Git operations - add all files and create single commit
for i, relativePath := range relativePaths {
// For host-specific files, we need to add the relative path from repo root
gitPath := relativePath
if l.host != "" {
gitPath = filepath.Join(l.host+".lnk", relativePath)
}
if err := l.git.Add(gitPath); err != nil {
// Rollback all operations
l.rollbackOperations(rollbackActions)
return fmt.Errorf("failed to add %s to git: %w", absolutePaths[i], err)
}
}
// Add .lnk file to the same commit
if err := l.git.Add(l.getLnkFileName()); err != nil {
// Rollback all operations
l.rollbackOperations(rollbackActions)
return fmt.Errorf("failed to add tracking file to git: %w", err)
}
// Commit all changes together
commitMessage := fmt.Sprintf("lnk: added %d files recursively", len(paths))
if err := l.git.Commit(commitMessage); err != nil {
// Rollback all operations
l.rollbackOperations(rollbackActions)
return fmt.Errorf("failed to commit changes: %w", err)
}
return nil
}
// AddRecursive adds directory contents individually instead of the directory as a whole
func (l *Lnk) AddRecursive(paths []string) error {
var allFiles []string
for _, path := range paths {
// Get absolute path
absPath, err := filepath.Abs(path)
if err != nil {
return fmt.Errorf("failed to get absolute path for %s: %w", path, err)
}
// Check if it's a directory
info, err := os.Stat(absPath)
if err != nil {
return fmt.Errorf("failed to stat %s: %w", path, err)
}
if info.IsDir() {
// Walk directory to get all files
files, err := l.walkDirectory(absPath)
if err != nil {
return fmt.Errorf("failed to walk directory %s: %w", path, err)
}
allFiles = append(allFiles, files...)
} else {
// It's a regular file, add it directly
allFiles = append(allFiles, absPath)
}
}
// Use AddMultiple for batch processing
if len(allFiles) == 0 {
return fmt.Errorf("no files found to add")
}
return l.AddMultiple(allFiles)
}