Skip to content

Commit 807cf54

Browse files
SSameer20schollz
andauthored
exact file-path exclusion support via --exclude-file (#1174)
Co-authored-by: Zack <zack.scholl@gmail.com>
1 parent ca03d6e commit 807cf54

4 files changed

Lines changed: 98 additions & 2 deletions

File tree

src/cli/cli.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ func Run() (err error) {
7979
&cli.IntFlag{Name: "transfers", Value: 4, Usage: "number of ports to use for transfers"},
8080
&cli.BoolFlag{Name: "qrcode", Aliases: []string{"qr"}, Usage: "show receive code as a qrcode"},
8181
&cli.StringFlag{Name: "exclude", Value: "", Usage: "exclude files if they contain any of the comma separated strings"},
82+
&cli.StringFlag{Name: "exclude-file", Value: "", Usage: "exclude files matching any of the comma separated relative paths exactly"},
8283
&cli.StringFlag{Name: "socks5", Value: "", Usage: "add a socks5 proxy", EnvVars: []string{"SOCKS5_PROXY"}},
8384
&cli.StringFlag{Name: "connect", Value: "", Usage: "add a http proxy", EnvVars: []string{"HTTP_PROXY"}},
8485
},
@@ -327,6 +328,13 @@ func send(c *cli.Context) (err error) {
327328
excludeStrings = append(excludeStrings, v)
328329
}
329330
}
331+
excludeFiles := []string{}
332+
for _, v := range strings.Split(c.String("exclude-file"), ",") {
333+
v = utils.NormalizeRelativePath(strings.TrimSpace(v))
334+
if v != "" && v != "." {
335+
excludeFiles = append(excludeFiles, v)
336+
}
337+
}
330338

331339
ports := make([]string, transfersParam+1)
332340
for i := 0; i <= transfersParam; i++ {
@@ -359,6 +367,7 @@ func send(c *cli.Context) (err error) {
359367
ShowQrCode: c.Bool("qrcode"),
360368
MulticastAddress: c.String("multicast"),
361369
Exclude: excludeStrings,
370+
ExcludeFile: excludeFiles,
362371
Quiet: c.Bool("quiet"),
363372
DisableClipboard: c.Bool("disable-clipboard"),
364373
ExtendedClipboard: c.Bool("extended-clipboard"),
@@ -475,7 +484,7 @@ Or you can go back to the classic croc behavior by enabling classic mode:
475484
// generate code phrase
476485
crocOptions.SharedSecret = utils.GetRandomName()
477486
}
478-
minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfo(fnames, crocOptions.ZipFolder, crocOptions.GitIgnore, crocOptions.Exclude)
487+
minimalFileInfos, emptyFoldersToTransfer, totalNumberFolders, err := croc.GetFilesInfoWithExactExclusions(fnames, crocOptions.ZipFolder, crocOptions.GitIgnore, crocOptions.Exclude, crocOptions.ExcludeFile)
479488
if err != nil {
480489
return
481490
}

src/croc/croc.go

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ type Options struct {
9999
MulticastAddress string
100100
ShowQrCode bool
101101
Exclude []string
102+
ExcludeFile []string
102103
Quiet bool
103104
DisableClipboard bool
104105
ExtendedClipboard bool
@@ -770,6 +771,12 @@ func isChild(parentPath, childPath string) bool {
770771
// This function retrieves the important file information
771772
// for every file that will be transferred
772773
func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []string) (filesInfo []FileInfo, emptyFolders []FileInfo, totalNumberFolders int, err error) {
774+
return GetFilesInfoWithExactExclusions(fnames, zipfolder, ignoreGit, exclusions, nil)
775+
}
776+
777+
// GetFilesInfoWithExactExclusions retrieves file information while applying
778+
// both the legacy substring exclusions and exact relative-path exclusions.
779+
func GetFilesInfoWithExactExclusions(fnames []string, zipfolder bool, ignoreGit bool, exclusions, exactExclusions []string) (filesInfo []FileInfo, emptyFolders []FileInfo, totalNumberFolders int, err error) {
773780
// fnames: the relative/absolute paths of files/folders that will be transferred
774781
totalNumberFolders = 0
775782
var paths []string
@@ -845,7 +852,7 @@ func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []
845852
}
846853
fpath = filepath.Dir(fpath)
847854
dest := filepath.Base(fpath) + ".zip"
848-
err = utils.ZipDirectory(dest, fpath, ignoredPaths, exclusions)
855+
err = utils.ZipDirectoryWithExactExclusions(dest, fpath, ignoredPaths, exclusions, exactExclusions)
849856
if err != nil {
850857
return
851858
}
@@ -891,6 +898,13 @@ func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []
891898
if strings.HasSuffix(absPathWithSeparator, string(os.PathSeparator)+string(os.PathSeparator)) {
892899
absPathWithSeparator = strings.TrimSuffix(absPathWithSeparator, string(os.PathSeparator))
893900
}
901+
relPath, relErr := filepath.Rel(absPath, pathName)
902+
if relErr == nil && exactPathExcluded(exactExclusions, relPath) {
903+
if info.IsDir() {
904+
return filepath.SkipDir
905+
}
906+
return nil
907+
}
894908
remoteFolder := strings.TrimPrefix(filepath.Dir(pathName), absPathWithSeparator)
895909
if !info.IsDir() {
896910
fInfo := FileInfo{
@@ -929,6 +943,9 @@ func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []
929943
}
930944

931945
} else {
946+
if exactPathExcluded(exactExclusions, stat.Name()) {
947+
continue
948+
}
932949
fInfo := FileInfo{
933950
Name: stat.Name(),
934951
FolderRemote: "./",
@@ -949,6 +966,16 @@ func GetFilesInfo(fnames []string, zipfolder bool, ignoreGit bool, exclusions []
949966
return
950967
}
951968

969+
func exactPathExcluded(exclusions []string, candidate string) bool {
970+
candidate = utils.NormalizeRelativePath(candidate)
971+
for _, exclusion := range exclusions {
972+
if candidate == utils.NormalizeRelativePath(exclusion) {
973+
return true
974+
}
975+
}
976+
return false
977+
}
978+
952979
func (c *Client) sendCollectFiles(filesInfo []FileInfo) (err error) {
953980
c.FilesToTransfer = filesInfo
954981
totalFilesSize := int64(0)

src/croc/croc_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,6 +768,40 @@ func TestGetFilesInfoZipFolderHonoursFilters(t *testing.T) {
768768
}
769769
}
770770

771+
func TestGetFilesInfoExactFileExclusion(t *testing.T) {
772+
root := filepath.Join(t.TempDir(), "root")
773+
for _, rel := range []string{"a/image.jpg", "b/a/image.jpg", "photo.png"} {
774+
file := filepath.Join(root, rel)
775+
if err := os.MkdirAll(filepath.Dir(file), 0o755); err != nil {
776+
t.Fatalf("mkdir: %v", err)
777+
}
778+
if err := os.WriteFile(file, []byte(rel), 0o644); err != nil {
779+
t.Fatalf("write %s: %v", rel, err)
780+
}
781+
}
782+
783+
files, _, _, err := GetFilesInfoWithExactExclusions([]string{root}, false, false, nil, []string{"a/image.jpg"})
784+
if err != nil {
785+
t.Fatalf("GetFilesInfoWithExactExclusions: %v", err)
786+
}
787+
got := make(map[string]bool)
788+
for _, file := range files {
789+
rel, err := filepath.Rel(root, filepath.Join(file.FolderSource, file.Name))
790+
if err != nil {
791+
t.Fatalf("relative path: %v", err)
792+
}
793+
got[filepath.ToSlash(rel)] = true
794+
}
795+
if got["a/image.jpg"] {
796+
t.Fatal("exactly excluded file was returned")
797+
}
798+
for _, want := range []string{"b/a/image.jpg", "photo.png"} {
799+
if !got[want] {
800+
t.Errorf("expected %q to be returned; got %v", want, got)
801+
}
802+
}
803+
}
804+
771805
// TestIsChild guards the gitignore walk helper. A directory or file whose
772806
// base name merely starts with ".." (e.g. "..cache") is still a legitimate
773807
// child of its parent and must be reported as such, otherwise it escapes the

src/utils/utils.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -508,6 +508,13 @@ func IsLocalIP(ipaddress string) bool {
508508
// any string in exclusions (case-insensitive) are also skipped, mirroring
509509
// the post-walk filter in cli.go for non-zip transfers.
510510
func ZipDirectory(destination string, source string, ignoredPaths map[string]bool, exclusions []string) (err error) {
511+
return ZipDirectoryWithExactExclusions(destination, source, ignoredPaths, exclusions, nil)
512+
}
513+
514+
// ZipDirectoryWithExactExclusions is ZipDirectory with support for exact
515+
// paths relative to source. Legacy exclusions remain case-insensitive
516+
// substring matches.
517+
func ZipDirectoryWithExactExclusions(destination string, source string, ignoredPaths map[string]bool, exclusions, exactExclusions []string) (err error) {
511518
if _, err = os.Stat(destination); err == nil {
512519
log.Errorf("%s file already exists!\n", destination)
513520
return fmt.Errorf("file already exists: %s", destination)
@@ -618,6 +625,7 @@ func ZipDirectory(destination string, source string, ignoredPaths map[string]boo
618625
// Create zip path with base name structure
619626
zipPath := filepath.Join(baseName, relPath)
620627
zipPath = filepath.ToSlash(zipPath)
628+
relPath = NormalizeRelativePath(relPath)
621629

622630
// Honour --exclude: case-insensitive substring match against the zip
623631
// path, mirroring the post-walk filter in cli.go.
@@ -632,6 +640,16 @@ func ZipDirectory(destination string, source string, ignoredPaths map[string]boo
632640
}
633641
}
634642
}
643+
if len(exactExclusions) > 0 {
644+
for _, exclusion := range exactExclusions {
645+
if relPath == NormalizeRelativePath(exclusion) {
646+
if info.IsDir() {
647+
return filepath.SkipDir
648+
}
649+
return nil
650+
}
651+
}
652+
}
635653

636654
if info.IsDir() {
637655
// Add directory entry to zip with original modification time
@@ -698,6 +716,14 @@ func ZipDirectory(destination string, source string, ignoredPaths map[string]boo
698716
return nil
699717
}
700718

719+
// NormalizeRelativePath converts a path to a portable, clean relative-path
720+
// representation suitable for exact comparisons.
721+
func NormalizeRelativePath(p string) string {
722+
p = filepath.ToSlash(filepath.Clean(p))
723+
p = strings.TrimPrefix(p, "./")
724+
return p
725+
}
726+
701727
func pathWithin(parent string, child string) bool {
702728
rel, err := filepath.Rel(filepath.Clean(parent), filepath.Clean(child))
703729
if err != nil {

0 commit comments

Comments
 (0)