diff --git a/jsync-engine/src/main/java/com/fizzed/jsync/engine/DefaultJsyncEventHandler.java b/jsync-engine/src/main/java/com/fizzed/jsync/engine/DefaultJsyncEventHandler.java index 455c9d5..74b2fb5 100644 --- a/jsync-engine/src/main/java/com/fizzed/jsync/engine/DefaultJsyncEventHandler.java +++ b/jsync-engine/src/main/java/com/fizzed/jsync/engine/DefaultJsyncEventHandler.java @@ -34,8 +34,13 @@ public void willExcludePath(VirtualPath sourcePath) { } @Override - public void willIgnorePath(VirtualPath sourcePath) { - log.debug("Ignoring path {}", sourcePath); + public void willIgnoreSourcePath(VirtualPath sourcePath) { + log.debug("Ignoring source path {}", sourcePath); + } + + @Override + public void willIgnoreTargetPath(VirtualPath targetPath) { + log.debug("Ignoring target path {}", targetPath); } @Override diff --git a/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEngine.java b/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEngine.java index 96c11cf..79f0589 100644 --- a/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEngine.java +++ b/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEngine.java @@ -2,6 +2,7 @@ import com.fizzed.jsync.vfs.*; import com.fizzed.jsync.vfs.util.Permissions; +import com.fizzed.jsync.vfs.util.VirtualPathMatchers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -31,9 +32,10 @@ public class JsyncEngine { private List ignores; // when running a sync private Checksum negotiatedChecksum; - private List excludePaths; - private List ignoreSourcePaths; - private List ignoreTargetPaths; + private VirtualPathMatchers excludeMatchers; + private VirtualPathMatchers ignoreMatchers; + private VirtualPath sourceRootPath; + private VirtualPath targetRootPath; public JsyncEngine() { this.eventHandler = new DefaultJsyncEventHandler(); @@ -225,6 +227,9 @@ public JsyncResult sync(VirtualFileSystem sourceVfs, String sourcePath, VirtualF final VirtualPath sourcePathAbsFinal = sourcePathAbs.normalize(); final VirtualPath targetPathAbsFinal = targetPathAbs.normalize(); + this.sourceRootPath = sourcePathAbsFinal; + this.targetRootPath = targetPathAbsFinal; + // // Negotiate checksum methods between source and target filesystems if necessary @@ -236,29 +241,12 @@ public JsyncResult sync(VirtualFileSystem sourceVfs, String sourcePath, VirtualF log.debug("Source filesystem stat mode: {}", sourceVfs.getStatModel()); log.debug("Target filesystem stat mode: {}", targetVfs.getStatModel()); - // build exclude and ignore paths - if (this.excludes != null) { - this.excludePaths = this.excludes.stream() - .map(VirtualPath::parse) - .map(sourcePathAbsFinal::resolve) - .collect(toList()); - } else { - this.excludePaths = Collections.emptyList(); - } - - if (this.ignores != null) { - this.ignoreSourcePaths = this.ignores.stream() - .map(VirtualPath::parse) - .map(sourcePathAbsFinal::resolve) - .collect(toList()); - this.ignoreTargetPaths = this.ignores.stream() - .map(VirtualPath::parse) - .map(targetPathAbsFinal::resolve) - .collect(toList()); - } else { - this.ignoreSourcePaths = Collections.emptyList(); - this.ignoreTargetPaths = Collections.emptyList(); - } + // build exclude and ignore matchers + this.excludeMatchers = VirtualPathMatchers.compile(this.excludes); + this.ignoreMatchers = VirtualPathMatchers.compile(this.ignores); + + log.debug("Using exclude matchers: {}", this.excludeMatchers); + log.debug("Using ignore matchers: {}", this.ignoreMatchers); final long now = System.currentTimeMillis(); @@ -272,17 +260,6 @@ public JsyncResult sync(VirtualFileSystem sourceVfs, String sourcePath, VirtualF final List deferredFiles = new ArrayList<>(); if (sourcePathAbsFinal.isDirectory()) { - // any excludes, let's resolve them against pwd of the source to make it easier to exclude them - final List excludePaths; - if (this.excludes != null) { - excludePaths = this.excludes.stream() - .map(VirtualPath::parse) - .map(sourcePathAbsFinal::resolve) - .collect(toList()); - } else { - excludePaths = Collections.emptyList(); - } - // as we process files, only a subset may require more advanced methods of detecting whether they were modified // since that process could be "expensive", we keep a list of files on source/target that we will defer processing // until we have a chance to do some bulk processing of checksums, etc. @@ -409,23 +386,20 @@ protected void syncDirectory(int level, JsyncResult result, List sourceChildPaths = sourceVfs.ls(sourcePath).stream() + final List sourceChildPaths = sourceVfs.ls(sourcePath).stream() // apply filter to source files if they are on the exclude list .filter(v -> { - for (VirtualPath p : this.excludePaths) { - if (v.startsWith(p)) { - this.eventHandler.willExcludePath(v); - return false; - } + if (this.excludeMatchers.matches(this.sourceRootPath, v)) { + this.eventHandler.willExcludePath(v); + return false; } return true; }) .filter(v -> { - for (VirtualPath p : this.ignoreSourcePaths) { - if (v.startsWith(p)) { - this.eventHandler.willIgnorePath(v); - return false; - } + log.debug("Checking if should ignore: root={}, path={}", this.sourceRootPath, v); + if (this.ignoreMatchers.matches(this.sourceRootPath, v)) { + this.eventHandler.willIgnoreSourcePath(v); + return false; } return true; }) @@ -446,10 +420,9 @@ protected void syncDirectory(int level, JsyncResult result, List targetChildPaths = targetVfs.ls(targetPath).stream() .filter(v -> { - for (VirtualPath p : this.ignoreTargetPaths) { - if (v.startsWith(p)) { - return false; - } + if (this.ignoreMatchers.matches(this.targetRootPath, v)) { + this.eventHandler.willIgnoreTargetPath(v); + return false; } return true; }) diff --git a/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEventHandler.java b/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEventHandler.java index f9f5c54..e3ab3af 100644 --- a/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEventHandler.java +++ b/jsync-engine/src/main/java/com/fizzed/jsync/engine/JsyncEventHandler.java @@ -16,9 +16,11 @@ public interface JsyncEventHandler { void willEnd(VirtualFileSystem sourceVfs, VirtualPath sourcePath, VirtualFileSystem targetVfs, VirtualPath targetPath, JsyncResult result, long timeMillis); - void willExcludePath(VirtualPath targetPath); + void willExcludePath(VirtualPath sourcePath); - void willIgnorePath(VirtualPath targetPath); + void willIgnoreSourcePath(VirtualPath sourcePath); + + void willIgnoreTargetPath(VirtualPath targetPath); void willCreateDirectory(VirtualPath targetPath, boolean recursively); diff --git a/jsync-vfs/src/main/java/com/fizzed/jsync/vfs/util/VirtualPathMatcher.java b/jsync-vfs/src/main/java/com/fizzed/jsync/vfs/util/VirtualPathMatcher.java new file mode 100644 index 0000000..fc726d3 --- /dev/null +++ b/jsync-vfs/src/main/java/com/fizzed/jsync/vfs/util/VirtualPathMatcher.java @@ -0,0 +1,101 @@ +package com.fizzed.jsync.vfs.util; + +import com.fizzed.jsync.vfs.VirtualPath; + +import java.nio.file.FileSystems; +import java.nio.file.PathMatcher; +import java.nio.file.Paths; + +public class VirtualPathMatcher { + + private final String globRule; + private final PathMatcher matcher; + + public VirtualPathMatcher(String globRule, PathMatcher matcher) { + this.globRule = globRule; + this.matcher = matcher; + } + + public boolean matches(VirtualPath rootPath, VirtualPath currentPath) { + final String relativePath; + + // resolve the current path against the root path, so we're left with the relative path we're matching against + if (currentPath.isAbsolute()) { + String rootFullPath = rootPath.toFullPath(); + String currentFullPath = currentPath.toFullPath(); + int pathStartPos = currentFullPath.indexOf(rootFullPath); + if (pathStartPos >= 0) { + // remove the leading path PLUS the file separator + relativePath = currentFullPath.substring(pathStartPos + rootFullPath.length() + 1); + } else { + relativePath = currentFullPath; + } + } else { + relativePath = currentPath.toString(); + } + + return this.matcher.matches(Paths.get(relativePath)); + } + + @Override + public String toString() { + return this.globRule; + } + + static public VirtualPathMatcher compile(String rule) { + String glob = rule.trim(); + boolean isDirectory = false; + boolean isRooted = false; + + // 1. Check for Directory marker + if (glob.endsWith("/")) { + isDirectory = true; + glob = glob.substring(0, glob.length() - 1); // Strip trailing slash + } + + // 2. Check for Root anchor + if (glob.startsWith("/")) { + isRooted = true; + glob = glob.substring(1); // Strip leading slash + } + + // FIX: Handle "/**/" usually found in the middle of paths + // Git: "docs/**/*.md" -> Zero or more dirs + // Java: "docs/**/*.md" -> One or more dirs (fails on docs/file.md) + // Solution: Replace "/**/" with "/{,**/}" which means "Empty OR /**/" +// if (glob.contains("/**/")) { +// glob = glob.replace("/**/", "/{,**/}"); +// } + + // 3. Build the Glob + // We need to construct a robust brace expansion {A,B,C...} + StringBuilder finalGlob = new StringBuilder(); + finalGlob.append("glob:{"); + + if (isRooted) { + // Rule: /target/ or /target + finalGlob.append(glob); // Matches "target" at root + if (isDirectory) { + finalGlob.append(",").append(glob).append("/**"); // Matches "target/..." at root + } + } else { + // Rule: target/ or target + finalGlob.append(glob); // Matches "target" at root + finalGlob.append(",**/").append(glob); // Matches "src/target" (nested) + + if (isDirectory) { + // If it's a directory, we must ALSO match the contents + finalGlob.append(",").append(glob).append("/**"); // Matches "target/file.txt" (root) + finalGlob.append(",**/").append(glob).append("/**"); // Matches "src/target/file.txt" (nested) + } + } + + finalGlob.append("}"); + + String globRule = finalGlob.toString(); + PathMatcher matcher = FileSystems.getDefault().getPathMatcher(globRule); + + return new VirtualPathMatcher(globRule, matcher); + } + +} \ No newline at end of file diff --git a/jsync-vfs/src/main/java/com/fizzed/jsync/vfs/util/VirtualPathMatchers.java b/jsync-vfs/src/main/java/com/fizzed/jsync/vfs/util/VirtualPathMatchers.java new file mode 100644 index 0000000..dd505a2 --- /dev/null +++ b/jsync-vfs/src/main/java/com/fizzed/jsync/vfs/util/VirtualPathMatchers.java @@ -0,0 +1,40 @@ +package com.fizzed.jsync.vfs.util; + +import com.fizzed.jsync.vfs.VirtualPath; + +import java.util.ArrayList; +import java.util.List; + +public class VirtualPathMatchers { + + private final List matchers; + + public VirtualPathMatchers(List matchers) { + this.matchers = matchers; + } + + public boolean matches(VirtualPath rootPath, VirtualPath path) { + for (VirtualPathMatcher matcher : this.matchers) { + if (matcher.matches(rootPath, path)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return this.matchers.toString(); + } + + static public VirtualPathMatchers compile(List rules) { + List matchers = new ArrayList<>(); + if (rules != null) { + for (String rule : rules) { + matchers.add(VirtualPathMatcher.compile(rule)); + } + } + return new VirtualPathMatchers(matchers); + } + +} \ No newline at end of file diff --git a/jsync-vfs/src/test/java/com/fizzed/jsync/vfs/util/VirtualPathMatcherTest.java b/jsync-vfs/src/test/java/com/fizzed/jsync/vfs/util/VirtualPathMatcherTest.java new file mode 100644 index 0000000..f94169a --- /dev/null +++ b/jsync-vfs/src/test/java/com/fizzed/jsync/vfs/util/VirtualPathMatcherTest.java @@ -0,0 +1,99 @@ +package com.fizzed.jsync.vfs.util; + +import com.fizzed.jsync.vfs.VirtualPath; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class VirtualPathMatcherTest { + + @Test + public void absoluteRootOnlyRuleForDirectory() { + VirtualPath rootPath = VirtualPath.parse("/home/jjlauer"); + + VirtualPathMatcher matcher = VirtualPathMatcher.compile("/target"); + + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target/a.txt"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target2"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/2target"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/sub/target"))).isFalse(); + } + + @Test + public void absoluteAnywhereRuleForDirectory() { + VirtualPath rootPath = VirtualPath.parse("/home/jjlauer"); + + VirtualPathMatcher matcher = VirtualPathMatcher.compile("target"); + + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target/a.txt"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target2"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/2target"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/sub/target"))).isTrue(); + } + + @Test + public void absoluteAnywhereRuleForDirectoryWithSlash() { + VirtualPath rootPath = VirtualPath.parse("/home/jjlauer"); + + VirtualPathMatcher matcher = VirtualPathMatcher.compile("target/"); + + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target/a.txt"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target2"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/2target"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/sub/target"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/sub/target/a.txt"))).isTrue(); + } + + @Test + public void absoluteAnywhereRuleForDirectoryWithLeadingDot() { + VirtualPath rootPath = VirtualPath.parse("/home/jjlauer"); + + VirtualPathMatcher matcher = VirtualPathMatcher.compile(".buildx"); + + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/.buildx"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/.buildx/a.txt"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target2"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/2target"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/sub/.buildx"))).isTrue(); + } + + @Test + public void absoluteAnywhereRuleForFile() { + VirtualPath rootPath = VirtualPath.parse("/home/jjlauer"); + + VirtualPathMatcher matcher = VirtualPathMatcher.compile("*.log"); + + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/target"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/log"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/a.log"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("/home/jjlauer/sub/a.log"))).isTrue(); + } + + @Test + public void absoluteAnywhereRuleForDirectoryOnWindows() { + VirtualPath rootPath = VirtualPath.parse("C:\\Users\\jjlauer"); + + VirtualPathMatcher matcher = VirtualPathMatcher.compile("target"); + + assertThat(matcher.matches(rootPath, VirtualPath.parse("C:\\Users\\jjlauer\\target"))).isTrue(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("C:\\Users\\jjlauer\\target\\a.txt"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("C:\\Users\\jjlauer\\target2"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("C:\\Users\\jjlauer\\2target"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("C:\\Users\\jjlauer\\sub\\target"))).isTrue(); + } + + @Test + public void relativeDoubleStarRuleForFiles() { + VirtualPath rootPath = VirtualPath.parse("/home/jjlauer"); + + VirtualPathMatcher matcher = VirtualPathMatcher.compile("docs/**/*.md"); + + // this is where java's globbing kinda fails where it can't do zero or more with that match + //assertThat(matcher.matches(rootPath, VirtualPath.parse("docs"))).isFalse(); + assertThat(matcher.matches(rootPath, VirtualPath.parse("docs/src/a.md"))).isTrue(); + } + +} \ No newline at end of file