refactor(errors): implement structured error handling for improved debugging

This commit is contained in:
Yar Kravtsov
2025-06-03 07:58:21 +03:00
parent 3cba309c05
commit 1e2c9704f3
15 changed files with 482 additions and 213 deletions

119
internal/fs/errors.go Normal file
View File

@@ -0,0 +1,119 @@
package fs
import "fmt"
// ANSI color codes for consistent formatting
const (
colorReset = "\033[0m"
colorRed = "\033[31m"
colorBold = "\033[1m"
)
// formatError creates a consistently formatted error message with ❌ prefix
func formatError(message string, args ...interface{}) string {
return fmt.Sprintf("❌ "+message, args...)
}
// formatPath formats a file path with red color
func formatPath(path string) string {
return fmt.Sprintf("%s%s%s", colorRed, path, colorReset)
}
// formatCommand formats a command with bold styling
func formatCommand(command string) string {
return fmt.Sprintf("%s%s%s", colorBold, command, colorReset)
}
// FileNotExistsError represents an error when a file does not exist
type FileNotExistsError struct {
Path string
Err error
}
func (e *FileNotExistsError) Error() string {
return formatError("File or directory not found: %s", formatPath(e.Path))
}
func (e *FileNotExistsError) Unwrap() error {
return e.Err
}
// FileCheckError represents an error when failing to check a file
type FileCheckError struct {
Err error
}
func (e *FileCheckError) Error() string {
return formatError("Unable to access file. Please check file permissions and try again.")
}
func (e *FileCheckError) Unwrap() error {
return e.Err
}
// UnsupportedFileTypeError represents an error when a file type is not supported
type UnsupportedFileTypeError struct {
Path string
}
func (e *UnsupportedFileTypeError) Error() string {
return formatError("Cannot manage this type of file: %s\n 💡 lnk can only manage regular files and directories", formatPath(e.Path))
}
func (e *UnsupportedFileTypeError) Unwrap() error {
return nil
}
// NotManagedByLnkError represents an error when a file is not managed by lnk
type NotManagedByLnkError struct {
Path string
}
func (e *NotManagedByLnkError) Error() string {
return formatError("File is not managed by lnk: %s\n 💡 Use %s to manage this file first",
formatPath(e.Path), formatCommand("lnk add"))
}
func (e *NotManagedByLnkError) Unwrap() error {
return nil
}
// SymlinkReadError represents an error when failing to read a symlink
type SymlinkReadError struct {
Err error
}
func (e *SymlinkReadError) Error() string {
return formatError("Unable to read symlink. The file may be corrupted or have invalid permissions.")
}
func (e *SymlinkReadError) Unwrap() error {
return e.Err
}
// DirectoryCreationError represents an error when failing to create a directory
type DirectoryCreationError struct {
Operation string
Err error
}
func (e *DirectoryCreationError) Error() string {
return formatError("Failed to create directory. Please check permissions and available disk space.")
}
func (e *DirectoryCreationError) Unwrap() error {
return e.Err
}
// RelativePathCalculationError represents an error when failing to calculate relative path
type RelativePathCalculationError struct {
Err error
}
func (e *RelativePathCalculationError) Error() string {
return formatError("Unable to create symlink due to path configuration issues. Please check file locations.")
}
func (e *RelativePathCalculationError) Unwrap() error {
return e.Err
}

View File

@@ -1,7 +1,6 @@
package fs
import (
"fmt"
"os"
"path/filepath"
"strings"
@@ -17,18 +16,19 @@ func New() *FileSystem {
// ValidateFileForAdd validates that a file or directory can be added to lnk
func (fs *FileSystem) ValidateFileForAdd(filePath string) error {
// Check if file exists
// Check if file exists and get its info
info, err := os.Stat(filePath)
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("❌ File does not exist: \033[31m%s\033[0m", filePath)
return &FileNotExistsError{Path: filePath, Err: err}
}
return fmt.Errorf("❌ Failed to check file: %w", err)
return &FileCheckError{Err: err}
}
// Allow both regular files and directories
if !info.Mode().IsRegular() && !info.IsDir() {
return fmt.Errorf("❌ Only regular files and directories are supported: \033[31m%s\033[0m", filePath)
return &UnsupportedFileTypeError{Path: filePath}
}
return nil
@@ -36,98 +36,79 @@ func (fs *FileSystem) ValidateFileForAdd(filePath string) error {
// ValidateSymlinkForRemove validates that a symlink can be removed from lnk
func (fs *FileSystem) ValidateSymlinkForRemove(filePath, repoPath string) error {
// Check if file exists
// Check if file exists and is a symlink
info, err := os.Lstat(filePath) // Use Lstat to not follow symlinks
if err != nil {
if os.IsNotExist(err) {
return fmt.Errorf("❌ File does not exist: \033[31m%s\033[0m", filePath)
return &FileNotExistsError{Path: filePath, Err: err}
}
return fmt.Errorf("❌ Failed to check file: %w", err)
return &FileCheckError{Err: err}
}
// Check if it's a symlink
if info.Mode()&os.ModeSymlink == 0 {
return fmt.Errorf("❌ File is not managed by lnk: \033[31m%s\033[0m\n 💡 Use \033[1mlnk add\033[0m to manage this file first", filePath)
return &NotManagedByLnkError{Path: filePath}
}
// Check if symlink points to the repository
// Get symlink target and resolve to absolute path
target, err := os.Readlink(filePath)
if err != nil {
return fmt.Errorf("failed to read symlink: %w", err)
return &SymlinkReadError{Err: err}
}
// Convert relative path to absolute if needed
if !filepath.IsAbs(target) {
target = filepath.Join(filepath.Dir(filePath), target)
}
// Clean the path to resolve any .. or . components
// Clean paths and check if target is inside the repository
target = filepath.Clean(target)
repoPath = filepath.Clean(repoPath)
// Check if target is inside the repository
if !strings.HasPrefix(target, repoPath+string(filepath.Separator)) && target != repoPath {
return fmt.Errorf("❌ File is not managed by lnk: \033[31m%s\033[0m", filePath)
return &NotManagedByLnkError{Path: filePath}
}
return nil
}
// Move moves a file or directory from source to destination based on the file info
func (fs *FileSystem) Move(src, dst string, info os.FileInfo) error {
if info.IsDir() {
return fs.MoveDirectory(src, dst)
}
return fs.MoveFile(src, dst)
}
// MoveFile moves a file from source to destination
func (fs *FileSystem) MoveFile(src, dst string) error {
// Ensure destination directory exists
dstDir := filepath.Dir(dst)
if err := os.MkdirAll(dstDir, 0755); err != nil {
return fmt.Errorf("failed to create destination directory: %w", err)
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return &DirectoryCreationError{Operation: "destination directory", Err: err}
}
// Move the file
if err := os.Rename(src, dst); err != nil {
return fmt.Errorf("failed to move file from %s to %s: %w", src, dst, err)
}
return nil
return os.Rename(src, dst)
}
// CreateSymlink creates a relative symlink from target to linkPath
func (fs *FileSystem) CreateSymlink(target, linkPath string) error {
// Calculate relative path from linkPath to target
linkDir := filepath.Dir(linkPath)
relTarget, err := filepath.Rel(linkDir, target)
relTarget, err := filepath.Rel(filepath.Dir(linkPath), target)
if err != nil {
return fmt.Errorf("failed to calculate relative path: %w", err)
return &RelativePathCalculationError{Err: err}
}
// Create the symlink
if err := os.Symlink(relTarget, linkPath); err != nil {
return fmt.Errorf("failed to create symlink: %w", err)
}
return nil
return os.Symlink(relTarget, linkPath)
}
// MoveDirectory moves a directory from source to destination recursively
func (fs *FileSystem) MoveDirectory(src, dst string) error {
// Check if source is a directory
info, err := os.Stat(src)
if err != nil {
return fmt.Errorf("failed to stat source: %w", err)
}
if !info.IsDir() {
return fmt.Errorf("source is not a directory: %s", src)
}
// Ensure destination parent directory exists
dstParent := filepath.Dir(dst)
if err := os.MkdirAll(dstParent, 0755); err != nil {
return fmt.Errorf("failed to create destination parent directory: %w", err)
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return &DirectoryCreationError{Operation: "destination parent directory", Err: err}
}
// Use os.Rename which works for directories
if err := os.Rename(src, dst); err != nil {
return fmt.Errorf("failed to move directory from %s to %s: %w", src, dst, err)
}
return nil
// Move the directory
return os.Rename(src, dst)
}