Skip to content

Commit e30b291

Browse files
Added support for symlinks and directories (#108)
* feat: add more templates * refactor: update the help section for the scaffold command with new templates * feat: add flags for recursive directory and hidden file / directory support * feat: add symlink and directory support for the add command
1 parent 6aa828f commit e30b291

3 files changed

Lines changed: 426 additions & 4 deletions

File tree

cmd/add.go

Lines changed: 136 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package cmd
66
import (
77
"context"
88
"fmt"
9+
"os"
910
"path/filepath"
1011
"strings"
1112
"time"
@@ -63,6 +64,16 @@ Examples:
6364
return err
6465
}
6566

67+
// Get recursive and includeHidden flags
68+
recursive, _ := cmd.Flags().GetBool("recursive")
69+
includeHidden, _ := cmd.Flags().GetBool("include-hidden")
70+
71+
// Expand directories if needed
72+
filePairs, err = expandDirectories(filePairs, recursive, includeHidden)
73+
if err != nil {
74+
return err
75+
}
76+
6677
// Get tags from flags
6778
tagsFlag, err := cmd.Flags().GetString("tags")
6879
if err != nil {
@@ -138,15 +149,61 @@ Examples:
138149
fmt.Printf("Processing: %s\n", pair.Source)
139150
}
140151

141-
// Check if file exists and that it is not a directory or symlink
142-
fileInfo, err := fs.VerifyFileAndReturnFileInfo(pair.Source)
152+
// Determine path type and handle accordingly
153+
fileInfo, pathType, err := fs.GetPathInfo(pair.Source)
143154
if err != nil {
144155
errorMsg := fmt.Sprintf("✗ %s: %v", filepath.Base(pair.Source), err)
145156
fmt.Println(errorMsg)
146157
failedFiles = append(failedFiles, errorMsg)
147158
continue
148159
}
149160

161+
// Handle different path types
162+
var actualSourcePath string
163+
switch pathType {
164+
case fs.PathTypeFile:
165+
// Regular file - use as is
166+
actualSourcePath = pair.Source
167+
168+
case fs.PathTypeSymlink:
169+
// Resolve symlink and verify target is a regular file
170+
targetPath, targetInfo, targetType, err := fs.ResolveSymlink(pair.Source)
171+
if err != nil {
172+
errorMsg := fmt.Sprintf("✗ %s: %v", filepath.Base(pair.Source), err)
173+
fmt.Println(errorMsg)
174+
failedFiles = append(failedFiles, errorMsg)
175+
continue
176+
}
177+
178+
if targetType != fs.PathTypeFile {
179+
errorMsg := fmt.Sprintf("✗ %s: symlink target is not a regular file", filepath.Base(pair.Source))
180+
fmt.Println(errorMsg)
181+
failedFiles = append(failedFiles, errorMsg)
182+
continue
183+
}
184+
185+
// Use the resolved target path for processing
186+
actualSourcePath = targetPath
187+
fileInfo = targetInfo
188+
189+
if verbose {
190+
fmt.Printf(" Resolved symlink: %s → %s\n", pair.Source, targetPath)
191+
}
192+
193+
case fs.PathTypeDir:
194+
// Directories should have been expanded already
195+
errorMsg := fmt.Sprintf("✗ %s: unexpected directory in processing loop", filepath.Base(pair.Source))
196+
fmt.Println(errorMsg)
197+
failedFiles = append(failedFiles, errorMsg)
198+
continue
199+
200+
default:
201+
errorMsg := fmt.Sprintf("✗ %s: unsupported file type", filepath.Base(pair.Source))
202+
fmt.Println(errorMsg)
203+
failedFiles = append(failedFiles, errorMsg)
204+
continue
205+
}
206+
150207
// Get file size in human-readable format
151208
sizeInBytes := fileInfo.Size()
152209
sizeReadable := util.HumanReadableSize(sizeInBytes)
@@ -163,7 +220,7 @@ Examples:
163220

164221
// Process the file and store chunks - using the appropriate chunking function
165222
var chunkRefs []config.ChunkRef
166-
chunkRefs, err = chunk.ChunkFile(ctx, pair.Source, chunkSize, vaultRoot, passphrase, progressMgr)
223+
chunkRefs, err = chunk.ChunkFile(ctx, actualSourcePath, chunkSize, vaultRoot, passphrase, progressMgr)
167224

168225
if err != nil {
169226
errorMsg := fmt.Sprintf("✗ %s: chunking failed - %v", filepath.Base(pair.Source), err)
@@ -357,15 +414,90 @@ func parseFileArguments(args []string) ([]FilePair, error) {
357414
return pairs, nil
358415
}
359416

417+
// expandDirectories expands directories into file pairs if recursive flag is set
418+
func expandDirectories(pairs []FilePair, recursive bool, includeHidden bool) ([]FilePair, error) {
419+
var expandedPairs []FilePair
420+
421+
for _, pair := range pairs {
422+
// Get path info to determine type
423+
fileInfo, pathType, err := fs.GetPathInfo(pair.Source)
424+
if err != nil {
425+
return nil, err
426+
}
427+
428+
switch pathType {
429+
case fs.PathTypeFile:
430+
// Regular file - add as is
431+
expandedPairs = append(expandedPairs, pair)
432+
433+
case fs.PathTypeSymlink:
434+
// Symlink - will be handled in processing loop, add as is
435+
expandedPairs = append(expandedPairs, pair)
436+
437+
case fs.PathTypeDir:
438+
// Directory - expand if recursive, otherwise error
439+
if !recursive {
440+
return nil, fmt.Errorf("'%s' is a directory. Use --recursive flag to add directories", pair.Source)
441+
}
442+
443+
// Walk the directory tree
444+
err := filepath.WalkDir(pair.Source, func(path string, d os.DirEntry, err error) error {
445+
if err != nil {
446+
return err
447+
}
448+
449+
// Skip hidden files/directories if includeHidden is false
450+
if fs.ShouldSkipHidden(d.Name(), includeHidden) {
451+
if d.IsDir() {
452+
return filepath.SkipDir
453+
}
454+
return nil
455+
}
456+
457+
// Only add regular files and symlinks
458+
if !d.IsDir() {
459+
// Compute relative path from source directory
460+
relPath, err := filepath.Rel(pair.Source, path)
461+
if err != nil {
462+
return fmt.Errorf("failed to compute relative path: %v", err)
463+
}
464+
465+
// Preserve directory structure in destination
466+
destPath := filepath.Join(pair.Destination, relPath)
467+
468+
expandedPairs = append(expandedPairs, FilePair{
469+
Source: path,
470+
Destination: destPath,
471+
})
472+
}
473+
474+
return nil
475+
})
476+
477+
if err != nil {
478+
return nil, fmt.Errorf("error walking directory '%s': %v", pair.Source, err)
479+
}
480+
481+
default:
482+
return nil, fmt.Errorf("'%s' is not a regular file, directory, or symlink", pair.Source)
483+
}
484+
485+
_ = fileInfo // fileInfo might be used for verbose output later
486+
}
487+
488+
return expandedPairs, nil
489+
}
490+
360491
func init() {
361492
rootCmd.AddCommand(addCmd)
362493

363494
// Optional flags for the add command
364495
addCmd.Flags().BoolP("force", "f", false, "Force add without confirmation")
365496
addCmd.Flags().StringP("tags", "t", "", "Comma-separated tags to associate with the file")
366497
addCmd.Flags().StringP("passphrase-value", "p", "", "Passphrase for encrypted vault (if required)")
498+
addCmd.Flags().BoolP("recursive", "r", false, "Recursively add directories")
499+
addCmd.Flags().BoolP("include-hidden", "H", false, "Include hidden files and directories")
367500
}
368501

369-
//TODO: Add support for directories and symlinks
370502
//TODO: Need to check how symlinks will be handled
371503
//TODO: Interactive mode with real time progress indicators

internal/fs/helper.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"fmt"
55
"os"
66
"path/filepath"
7+
"strings"
78
)
89

910
// EnsureDirectory ensures a directory exists, creating it if necessary
@@ -89,3 +90,64 @@ func VerifyFileAndReturnFile(filePath string) (*os.File, error) {
8990
}
9091
return file, nil
9192
}
93+
94+
// PathType represents the type of a file system path
95+
type PathType int
96+
97+
const (
98+
PathTypeFile PathType = iota
99+
PathTypeDir
100+
PathTypeSymlink
101+
PathTypeOther
102+
)
103+
104+
// GetPathInfo returns file info and the type of path (file/dir/symlink)
105+
func GetPathInfo(path string) (os.FileInfo, PathType, error) {
106+
// Use Lstat to get info about the path itself (not following symlinks)
107+
fileInfo, err := os.Lstat(path)
108+
if err != nil {
109+
if os.IsNotExist(err) {
110+
return nil, PathTypeOther, fmt.Errorf("path does not exist: %s", path)
111+
}
112+
return nil, PathTypeOther, fmt.Errorf("error accessing path: %v", err)
113+
}
114+
115+
// Determine path type
116+
mode := fileInfo.Mode()
117+
switch {
118+
case mode&os.ModeSymlink != 0:
119+
return fileInfo, PathTypeSymlink, nil
120+
case mode.IsDir():
121+
return fileInfo, PathTypeDir, nil
122+
case mode.IsRegular():
123+
return fileInfo, PathTypeFile, nil
124+
default:
125+
return fileInfo, PathTypeOther, fmt.Errorf("unsupported file type: %s", path)
126+
}
127+
}
128+
129+
// ResolveSymlink resolves a symlink to its target path and returns the target's info and type
130+
func ResolveSymlink(symlinkPath string) (targetPath string, targetInfo os.FileInfo, targetType PathType, err error) {
131+
// Resolve the symlink
132+
targetPath, err = filepath.EvalSymlinks(symlinkPath)
133+
if err != nil {
134+
return "", nil, PathTypeOther, fmt.Errorf("failed to resolve symlink: %v", err)
135+
}
136+
137+
// Get info about the target
138+
targetInfo, targetType, err = GetPathInfo(targetPath)
139+
if err != nil {
140+
return "", nil, PathTypeOther, fmt.Errorf("symlink target error: %v", err)
141+
}
142+
143+
return targetPath, targetInfo, targetType, nil
144+
}
145+
146+
// ShouldSkipHidden determines if a file/directory should be skipped based on hidden file rules
147+
func ShouldSkipHidden(name string, includeHidden bool) bool {
148+
if includeHidden {
149+
return false
150+
}
151+
// Skip files/directories starting with '.' (hidden on Unix-like systems)
152+
return strings.HasPrefix(name, ".")
153+
}

0 commit comments

Comments
 (0)