Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 136 additions & 4 deletions cmd/add.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ package cmd
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
Expand Down Expand Up @@ -63,6 +64,16 @@ Examples:
return err
}

// Get recursive and includeHidden flags
recursive, _ := cmd.Flags().GetBool("recursive")
includeHidden, _ := cmd.Flags().GetBool("include-hidden")

// Expand directories if needed
filePairs, err = expandDirectories(filePairs, recursive, includeHidden)
if err != nil {
return err
}

// Get tags from flags
tagsFlag, err := cmd.Flags().GetString("tags")
if err != nil {
Expand Down Expand Up @@ -138,15 +149,61 @@ Examples:
fmt.Printf("Processing: %s\n", pair.Source)
}

// Check if file exists and that it is not a directory or symlink
fileInfo, err := fs.VerifyFileAndReturnFileInfo(pair.Source)
// Determine path type and handle accordingly
fileInfo, pathType, err := fs.GetPathInfo(pair.Source)
if err != nil {
errorMsg := fmt.Sprintf("✗ %s: %v", filepath.Base(pair.Source), err)
fmt.Println(errorMsg)
failedFiles = append(failedFiles, errorMsg)
continue
}

// Handle different path types
var actualSourcePath string
switch pathType {
case fs.PathTypeFile:
// Regular file - use as is
actualSourcePath = pair.Source

case fs.PathTypeSymlink:
// Resolve symlink and verify target is a regular file
targetPath, targetInfo, targetType, err := fs.ResolveSymlink(pair.Source)
if err != nil {
errorMsg := fmt.Sprintf("✗ %s: %v", filepath.Base(pair.Source), err)
fmt.Println(errorMsg)
failedFiles = append(failedFiles, errorMsg)
continue
}

if targetType != fs.PathTypeFile {
errorMsg := fmt.Sprintf("✗ %s: symlink target is not a regular file", filepath.Base(pair.Source))
fmt.Println(errorMsg)
failedFiles = append(failedFiles, errorMsg)
continue
}

// Use the resolved target path for processing
actualSourcePath = targetPath
fileInfo = targetInfo

if verbose {
fmt.Printf(" Resolved symlink: %s → %s\n", pair.Source, targetPath)
}

case fs.PathTypeDir:
// Directories should have been expanded already
errorMsg := fmt.Sprintf("✗ %s: unexpected directory in processing loop", filepath.Base(pair.Source))
fmt.Println(errorMsg)
failedFiles = append(failedFiles, errorMsg)
continue

default:
errorMsg := fmt.Sprintf("✗ %s: unsupported file type", filepath.Base(pair.Source))
fmt.Println(errorMsg)
failedFiles = append(failedFiles, errorMsg)
continue
}

// Get file size in human-readable format
sizeInBytes := fileInfo.Size()
sizeReadable := util.HumanReadableSize(sizeInBytes)
Expand All @@ -163,7 +220,7 @@ Examples:

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

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

// expandDirectories expands directories into file pairs if recursive flag is set
func expandDirectories(pairs []FilePair, recursive bool, includeHidden bool) ([]FilePair, error) {
var expandedPairs []FilePair

for _, pair := range pairs {
// Get path info to determine type
fileInfo, pathType, err := fs.GetPathInfo(pair.Source)
if err != nil {
return nil, err
}

switch pathType {
case fs.PathTypeFile:
// Regular file - add as is
expandedPairs = append(expandedPairs, pair)

case fs.PathTypeSymlink:
// Symlink - will be handled in processing loop, add as is
expandedPairs = append(expandedPairs, pair)

case fs.PathTypeDir:
// Directory - expand if recursive, otherwise error
if !recursive {
return nil, fmt.Errorf("'%s' is a directory. Use --recursive flag to add directories", pair.Source)
}

// Walk the directory tree
err := filepath.WalkDir(pair.Source, func(path string, d os.DirEntry, err error) error {
if err != nil {
return err
}

// Skip hidden files/directories if includeHidden is false
if fs.ShouldSkipHidden(d.Name(), includeHidden) {
if d.IsDir() {
return filepath.SkipDir
}
return nil
}

// Only add regular files and symlinks
if !d.IsDir() {
// Compute relative path from source directory
relPath, err := filepath.Rel(pair.Source, path)
if err != nil {
return fmt.Errorf("failed to compute relative path: %v", err)
}

// Preserve directory structure in destination
destPath := filepath.Join(pair.Destination, relPath)

expandedPairs = append(expandedPairs, FilePair{
Source: path,
Destination: destPath,
})
}

return nil
})

if err != nil {
return nil, fmt.Errorf("error walking directory '%s': %v", pair.Source, err)
}

default:
return nil, fmt.Errorf("'%s' is not a regular file, directory, or symlink", pair.Source)
}

_ = fileInfo // fileInfo might be used for verbose output later
}

return expandedPairs, nil
}

func init() {
rootCmd.AddCommand(addCmd)

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

//TODO: Add support for directories and symlinks
//TODO: Need to check how symlinks will be handled
//TODO: Interactive mode with real time progress indicators
2 changes: 1 addition & 1 deletion cmd/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ Example:
}

// Create sync service (with or without RSA)
syncService, err := discover.CreateSyncService(host, vaultMgr, vaultConfig, vaultPath)
syncService, err := discover.CreateSyncService(host, vaultMgr, vaultConfig, vaultPath, verbose)
if err != nil {
return fmt.Errorf("failed to create sync service: %v", err)
}
Expand Down
5 changes: 5 additions & 0 deletions cmd/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ Examples:
return fmt.Errorf("failed to create sync service: %v", err)
}

// Set verbose flag
verbose, _ := cmd.Flags().GetBool("verbose")
syncService.Verbose = verbose

// Start secure protocol handlers
syncService.RegisterProtocols(ctx)

Expand Down Expand Up @@ -339,4 +343,5 @@ func init() {
syncCmd.Flags().IntP("timeout", "t", 60, "Discovery timeout in seconds (for auto-discovery)")
syncCmd.Flags().BoolP("force-trust", "f", false, "Automatically trust new peers without prompting")
syncCmd.Flags().BoolP("read-only", "r", false, "Only receive files, don't send")
syncCmd.Flags().BoolP("verbose", "v", false, "Enable verbose debug output")
}
14 changes: 9 additions & 5 deletions internal/discover/discovery_util.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,29 +14,33 @@ import (
)

// createSyncService creates a sync service with or without RSA support
func CreateSyncService(h host.Host, vaultMgr *config.Manager, vaultConfig *config.VaultConfig, vaultPath string) (*p2p.SyncService, error) {
func CreateSyncService(h host.Host, vaultMgr *config.Manager, vaultConfig *config.VaultConfig, vaultPath string, verbose bool) (*p2p.SyncService, error) {
var syncService *p2p.SyncService
var err error

if vaultConfig.Sync.Enabled && vaultConfig.Sync.RSA != nil {
privateKey, publicKey, rsaConfig, err := keys.LoadRSAKeys(vaultPath, vaultConfig.Sync.RSA)
if err != nil {
return nil, fmt.Errorf("failed to load RSA keys: %v", err)
}

syncService, err := p2p.NewSecureSyncService(h, vaultMgr, privateKey, publicKey, rsaConfig)
syncService, err = p2p.NewSecureSyncService(h, vaultMgr, privateKey, publicKey, rsaConfig)
if err != nil {
return nil, fmt.Errorf("failed to create sync service: %v", err)
}

fmt.Println("🔐 RSA key exchange enabled with fingerprint:", rsaConfig.Fingerprint)
return syncService, nil
} else {
syncService, err := p2p.NewSyncService(h, vaultMgr)
syncService, err = p2p.NewSyncService(h, vaultMgr)
if err != nil {
return nil, fmt.Errorf("failed to create sync service: %v", err)
}

fmt.Println("⚠️ Warning: RSA key exchange not enabled in vault config")
return syncService, nil
}

syncService.Verbose = verbose
return syncService, nil
}

func SetupDiscovery(ctx context.Context, h host.Host) (*p2p.MDNSDiscovery, <-chan peer.AddrInfo, error) {
Expand Down
2 changes: 1 addition & 1 deletion internal/discover/discovery_util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestCreateSyncService_NoRSA(t *testing.T) {
vc := &config.VaultConfig{}
vc.Sync.Enabled = false

svc, err := CreateSyncService(h, vm, vc, ".")
svc, err := CreateSyncService(h, vm, vc, ".", false)
if err != nil {
t.Fatalf("CreateSyncService returned error: %v", err)
}
Expand Down
2 changes: 1 addition & 1 deletion internal/discover/handle_discovered_peer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func TestHandleDiscoveredPeerExercisesPaths(t *testing.T) {

// Create non-RSA sync service
vc := &config.VaultConfig{}
svc, err := CreateSyncService(h, vm, vc, ".")
svc, err := CreateSyncService(h, vm, vc, ".", false)
if err != nil {
t.Fatalf("CreateSyncService failed: %v", err)
}
Expand Down
62 changes: 62 additions & 0 deletions internal/fs/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
)

// EnsureDirectory ensures a directory exists, creating it if necessary
Expand Down Expand Up @@ -89,3 +90,64 @@ func VerifyFileAndReturnFile(filePath string) (*os.File, error) {
}
return file, nil
}

// PathType represents the type of a file system path
type PathType int

const (
PathTypeFile PathType = iota
PathTypeDir
PathTypeSymlink
PathTypeOther
)

// GetPathInfo returns file info and the type of path (file/dir/symlink)
func GetPathInfo(path string) (os.FileInfo, PathType, error) {
// Use Lstat to get info about the path itself (not following symlinks)
fileInfo, err := os.Lstat(path)
if err != nil {
if os.IsNotExist(err) {
return nil, PathTypeOther, fmt.Errorf("path does not exist: %s", path)
}
return nil, PathTypeOther, fmt.Errorf("error accessing path: %v", err)
}

// Determine path type
mode := fileInfo.Mode()
switch {
case mode&os.ModeSymlink != 0:
return fileInfo, PathTypeSymlink, nil
case mode.IsDir():
return fileInfo, PathTypeDir, nil
case mode.IsRegular():
return fileInfo, PathTypeFile, nil
default:
return fileInfo, PathTypeOther, fmt.Errorf("unsupported file type: %s", path)
}
}

// ResolveSymlink resolves a symlink to its target path and returns the target's info and type
func ResolveSymlink(symlinkPath string) (targetPath string, targetInfo os.FileInfo, targetType PathType, err error) {
// Resolve the symlink
targetPath, err = filepath.EvalSymlinks(symlinkPath)
if err != nil {
return "", nil, PathTypeOther, fmt.Errorf("failed to resolve symlink: %v", err)
}

// Get info about the target
targetInfo, targetType, err = GetPathInfo(targetPath)
if err != nil {
return "", nil, PathTypeOther, fmt.Errorf("symlink target error: %v", err)
}

return targetPath, targetInfo, targetType, nil
}

// ShouldSkipHidden determines if a file/directory should be skipped based on hidden file rules
func ShouldSkipHidden(name string, includeHidden bool) bool {
if includeHidden {
return false
}
// Skip files/directories starting with '.' (hidden on Unix-like systems)
return strings.HasPrefix(name, ".")
}
Loading
Loading