Move CMS file utilities to a dedicated package
authorMathieu Baudier <mbaudier@argeo.org>
Wed, 20 Jul 2022 09:39:17 +0000 (11:39 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Wed, 20 Jul 2022 09:39:17 +0000 (11:39 +0200)
org.argeo.cms.cli/src/org/argeo/cms/cli/FileSync.java
org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java [deleted file]
org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java [deleted file]
org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java [deleted file]
org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java [deleted file]
org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java [deleted file]
org.argeo.cms/src/org/argeo/cms/file/BasicSyncFileVisitor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/file/FsSyncUtils.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/file/PathSync.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/file/SyncFileVisitor.java [new file with mode: 0644]
org.argeo.cms/src/org/argeo/cms/file/SyncResult.java [new file with mode: 0644]

index ccd092430af066c918af4173cd50ddfba35552d2..dc4c689d078a8731107e6c124fafae7ce9d88ab1 100644 (file)
@@ -11,8 +11,8 @@ import org.apache.commons.cli.Option;
 import org.apache.commons.cli.Options;
 import org.argeo.api.cli.CommandArgsException;
 import org.argeo.api.cli.DescribedCommand;
-import org.argeo.cms.acr.fs.PathSync;
-import org.argeo.cms.acr.fs.SyncResult;
+import org.argeo.cms.file.PathSync;
+import org.argeo.cms.file.SyncResult;
 
 public class FileSync implements DescribedCommand<SyncResult<Path>> {
        final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build();
diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/BasicSyncFileVisitor.java
deleted file mode 100644 (file)
index 7041c75..0000000
+++ /dev/null
@@ -1,162 +0,0 @@
-package org.argeo.cms.acr.fs;
-
-import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.StandardCopyOption;
-import java.nio.file.attribute.BasicFileAttributes;
-import java.nio.file.attribute.FileTime;
-
-/** Synchronises two directory structures. */
-public class BasicSyncFileVisitor extends SimpleFileVisitor<Path> {
-       // TODO make it configurable
-       private boolean trace = false;
-
-       private final Path sourceBasePath;
-       private final Path targetBasePath;
-       private final boolean delete;
-       private final boolean recursive;
-
-       private SyncResult<Path> syncResult = new SyncResult<>();
-
-       public BasicSyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) {
-               this.sourceBasePath = sourceBasePath;
-               this.targetBasePath = targetBasePath;
-               this.delete = delete;
-               this.recursive = recursive;
-       }
-
-       @Override
-       public FileVisitResult preVisitDirectory(Path sourceDir, BasicFileAttributes attrs) throws IOException {
-               if (!recursive && !sourceDir.equals(sourceBasePath))
-                       return FileVisitResult.SKIP_SUBTREE;
-               Path targetDir = toTargetPath(sourceDir);
-               Files.createDirectories(targetDir);
-               return FileVisitResult.CONTINUE;
-       }
-
-       @Override
-       public FileVisitResult postVisitDirectory(Path sourceDir, IOException exc) throws IOException {
-               if (delete) {
-                       Path targetDir = toTargetPath(sourceDir);
-                       for (Path targetPath : Files.newDirectoryStream(targetDir)) {
-                               Path sourcePath = sourceDir.resolve(targetPath.getFileName());
-                               if (!Files.exists(sourcePath)) {
-                                       try {
-                                               FsSyncUtils.delete(targetPath);
-                                               deleted(targetPath);
-                                       } catch (Exception e) {
-                                               deleteFailed(targetPath, exc);
-                                       }
-                               }
-                       }
-               }
-               return FileVisitResult.CONTINUE;
-       }
-
-       @Override
-       public FileVisitResult visitFile(Path sourceFile, BasicFileAttributes attrs) throws IOException {
-               Path targetFile = toTargetPath(sourceFile);
-               try {
-                       if (!Files.exists(targetFile)) {
-                               Files.copy(sourceFile, targetFile);
-                               added(sourceFile, targetFile);
-                       } else {
-                               if (shouldOverwrite(sourceFile, targetFile)) {
-                                       Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
-                               }
-                       }
-               } catch (Exception e) {
-                       copyFailed(sourceFile, targetFile, e);
-               }
-               return FileVisitResult.CONTINUE;
-       }
-
-       protected boolean shouldOverwrite(Path sourceFile, Path targetFile) throws IOException {
-               long sourceSize = Files.size(sourceFile);
-               long targetSize = Files.size(targetFile);
-               if (sourceSize != targetSize) {
-                       return true;
-               }
-               FileTime sourceLastModif = Files.getLastModifiedTime(sourceFile);
-               FileTime targetLastModif = Files.getLastModifiedTime(targetFile);
-               if (sourceLastModif.compareTo(targetLastModif) > 0)
-                       return true;
-               return shouldOverwriteLaterSameSize(sourceFile, targetFile);
-       }
-
-       protected boolean shouldOverwriteLaterSameSize(Path sourceFile, Path targetFile) {
-               return false;
-       }
-
-//     @Override
-//     public FileVisitResult visitFileFailed(Path sourceFile, IOException exc) throws IOException {
-//             error("Cannot sync " + sourceFile, exc);
-//             return FileVisitResult.CONTINUE;
-//     }
-
-       private Path toTargetPath(Path sourcePath) {
-               Path relativePath = sourceBasePath.relativize(sourcePath);
-               Path targetPath = targetBasePath.resolve(relativePath.toString());
-               return targetPath;
-       }
-
-       public Path getSourceBasePath() {
-               return sourceBasePath;
-       }
-
-       public Path getTargetBasePath() {
-               return targetBasePath;
-       }
-
-       protected void added(Path sourcePath, Path targetPath) {
-               syncResult.getAdded().add(targetPath);
-               if (isTraceEnabled())
-                       trace("Added " + sourcePath + " as " + targetPath);
-       }
-
-       protected void modified(Path sourcePath, Path targetPath) {
-               syncResult.getModified().add(targetPath);
-               if (isTraceEnabled())
-                       trace("Overwritten from " + sourcePath + " to " + targetPath);
-       }
-
-       protected void copyFailed(Path sourcePath, Path targetPath, Exception e) {
-               syncResult.addError(sourcePath, targetPath, e);
-               if (isTraceEnabled())
-                       error("Cannot copy " + sourcePath + " to " + targetPath, e);
-       }
-
-       protected void deleted(Path targetPath) {
-               syncResult.getDeleted().add(targetPath);
-               if (isTraceEnabled())
-                       trace("Deleted " + targetPath);
-       }
-
-       protected void deleteFailed(Path targetPath, Exception e) {
-               syncResult.addError(null, targetPath, e);
-               if (isTraceEnabled())
-                       error("Cannot delete " + targetPath, e);
-       }
-
-       /** Log error. */
-       protected void error(Object obj, Throwable e) {
-               System.err.println(obj);
-               e.printStackTrace();
-       }
-
-       protected boolean isTraceEnabled() {
-               return trace;
-       }
-
-       protected void trace(Object obj) {
-               System.out.println(obj);
-       }
-
-       public SyncResult<Path> getSyncResult() {
-               return syncResult;
-       }
-
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/FsSyncUtils.java
deleted file mode 100644 (file)
index c45e66c..0000000
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.argeo.cms.acr.fs;
-
-import java.io.IOException;
-import java.nio.file.FileVisitResult;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.SimpleFileVisitor;
-import java.nio.file.attribute.BasicFileAttributes;
-
-public class FsSyncUtils {
-       /** Sync a source path with a target path. */
-       public static void sync(Path sourceBasePath, Path targetBasePath) {
-               sync(sourceBasePath, targetBasePath, false);
-       }
-
-       /** Sync a source path with a target path. */
-       public static void sync(Path sourceBasePath, Path targetBasePath, boolean delete) {
-               sync(new BasicSyncFileVisitor(sourceBasePath, targetBasePath, delete, true));
-       }
-
-       public static void sync(BasicSyncFileVisitor syncFileVisitor) {
-               try {
-                       Files.walkFileTree(syncFileVisitor.getSourceBasePath(), syncFileVisitor);
-               } catch (Exception e) {
-                       throw new RuntimeException("Cannot sync " + syncFileVisitor.getSourceBasePath() + " with "
-                                       + syncFileVisitor.getTargetBasePath(), e);
-               }
-       }
-
-       /**
-        * Deletes this path, recursively if needed. Does nothing if the path does not
-        * exist.
-        */
-       public static void delete(Path path) {
-               try {
-                       if (!Files.exists(path))
-                               return;
-                       Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
-                               @Override
-                               public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
-                                       if (e != null)
-                                               throw e;
-                                       Files.delete(directory);
-                                       return FileVisitResult.CONTINUE;
-                               }
-
-                               @Override
-                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
-                                       Files.delete(file);
-                                       return FileVisitResult.CONTINUE;
-                               }
-                       });
-               } catch (IOException e) {
-                       throw new RuntimeException("Cannot delete " + path, e);
-               }
-       }
-
-       /** Singleton. */
-       private FsSyncUtils() {
-
-       }
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/PathSync.java
deleted file mode 100644 (file)
index 427044e..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-package org.argeo.cms.acr.fs;
-
-import java.net.URI;
-import java.nio.file.FileSystems;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.spi.FileSystemProvider;
-import java.util.concurrent.Callable;
-
-/** Synchronises two paths. */
-public class PathSync implements Callable<SyncResult<Path>> {
-       private final URI sourceUri, targetUri;
-       private final boolean delete;
-       private final boolean recursive;
-
-       public PathSync(URI sourceUri, URI targetUri) {
-               this(sourceUri, targetUri, false, false);
-       }
-
-       public PathSync(URI sourceUri, URI targetUri, boolean delete, boolean recursive) {
-               this.sourceUri = sourceUri;
-               this.targetUri = targetUri;
-               this.delete = delete;
-               this.recursive = recursive;
-       }
-
-       @Override
-       public SyncResult<Path> call() {
-               try {
-                       Path sourceBasePath = createPath(sourceUri);
-                       Path targetBasePath = createPath(targetUri);
-                       SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath, delete, recursive);
-                       Files.walkFileTree(sourceBasePath, syncFileVisitor);
-                       return syncFileVisitor.getSyncResult();
-               } catch (Exception e) {
-                       throw new IllegalStateException("Cannot sync " + sourceUri + " to " + targetUri, e);
-               }
-       }
-
-       private Path createPath(URI uri) {
-               Path path;
-               if (uri.getScheme() == null) {
-                       path = Paths.get(uri.getPath());
-               } else if (uri.getScheme().equals("file")) {
-                       FileSystemProvider fsProvider = FileSystems.getDefault().provider();
-                       path = fsProvider.getPath(uri);
-               } else if (uri.getScheme().equals("davex")) {
-                       throw new UnsupportedOperationException();
-//                     FileSystemProvider fsProvider = new DavexFsProvider();
-//                     path = fsProvider.getPath(uri);
-//             } else if (uri.getScheme().equals("sftp")) {
-//                     Sftp sftp = new Sftp(uri);
-//                     path = sftp.getBasePath();
-               } else
-                       throw new IllegalArgumentException("URI scheme not supported for " + uri);
-               return path;
-       }
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncFileVisitor.java
deleted file mode 100644 (file)
index 2a37287..0000000
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.argeo.cms.acr.fs;
-
-import java.nio.file.Path;
-import java.util.Objects;
-
-import org.argeo.api.cms.CmsLog;
-
-/** Synchronises two directory structures. */
-public class SyncFileVisitor extends BasicSyncFileVisitor {
-       private final static CmsLog log = CmsLog.getLog(SyncFileVisitor.class);
-
-       public SyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) {
-               super(sourceBasePath, targetBasePath, delete, recursive);
-       }
-
-       @Override
-       protected void error(Object obj, Throwable e) {
-               log.error(Objects.toString(obj), e);
-       }
-
-       @Override
-       protected boolean isTraceEnabled() {
-               return log.isTraceEnabled();
-       }
-
-       @Override
-       protected void trace(Object obj) {
-               log.error(Objects.toString(obj));
-       }
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java b/org.argeo.cms/src/org/argeo/cms/acr/fs/SyncResult.java
deleted file mode 100644 (file)
index 709b485..0000000
+++ /dev/null
@@ -1,101 +0,0 @@
-package org.argeo.cms.acr.fs;
-
-import java.time.Instant;
-import java.util.Set;
-import java.util.TreeSet;
-
-/** Describes what happendend during a sync operation. */
-public class SyncResult<T> {
-       private final Set<T> added = new TreeSet<>();
-       private final Set<T> modified = new TreeSet<>();
-       private final Set<T> deleted = new TreeSet<>();
-       private final Set<Error> errors = new TreeSet<>();
-
-       public Set<T> getAdded() {
-               return added;
-       }
-
-       public Set<T> getModified() {
-               return modified;
-       }
-
-       public Set<T> getDeleted() {
-               return deleted;
-       }
-
-       public Set<Error> getErrors() {
-               return errors;
-       }
-
-       public void addError(T sourcePath, T targetPath, Exception e) {
-               Error error = new Error(sourcePath, targetPath, e);
-               errors.add(error);
-       }
-
-       public boolean noModification() {
-               return modified.isEmpty() && deleted.isEmpty() && added.isEmpty();
-       }
-
-       @Override
-       public String toString() {
-               if (noModification())
-                       return "No modification.";
-               StringBuffer sb = new StringBuffer();
-               for (T p : modified)
-                       sb.append("MOD ").append(p).append('\n');
-               for (T p : deleted)
-                       sb.append("DEL ").append(p).append('\n');
-               for (T p : added)
-                       sb.append("ADD ").append(p).append('\n');
-               for (Error error : errors)
-                       sb.append(error).append('\n');
-               return sb.toString();
-       }
-
-       public class Error implements Comparable<Error> {
-               private final T sourcePath;// if null this is a failed delete
-               private final T targetPath;
-               private final Exception exception;
-               private final Instant timestamp = Instant.now();
-
-               public Error(T sourcePath, T targetPath, Exception e) {
-                       super();
-                       this.sourcePath = sourcePath;
-                       this.targetPath = targetPath;
-                       this.exception = e;
-               }
-
-               public T getSourcePath() {
-                       return sourcePath;
-               }
-
-               public T getTargetPath() {
-                       return targetPath;
-               }
-
-               public Exception getException() {
-                       return exception;
-               }
-
-               public Instant getTimestamp() {
-                       return timestamp;
-               }
-
-               @Override
-               public int compareTo(Error o) {
-                       return timestamp.compareTo(o.timestamp);
-               }
-
-               @Override
-               public int hashCode() {
-                       return timestamp.hashCode();
-               }
-
-               @Override
-               public String toString() {
-                       return "ERR " + timestamp + (sourcePath == null ? "Deletion failed" : "Copy failed " + sourcePath) + " "
-                                       + targetPath + " " + exception.getMessage();
-               }
-
-       }
-}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/BasicSyncFileVisitor.java b/org.argeo.cms/src/org/argeo/cms/file/BasicSyncFileVisitor.java
new file mode 100644 (file)
index 0000000..ac81329
--- /dev/null
@@ -0,0 +1,162 @@
+package org.argeo.cms.file;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.StandardCopyOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+
+/** Synchronises two directory structures. */
+public class BasicSyncFileVisitor extends SimpleFileVisitor<Path> {
+       // TODO make it configurable
+       private boolean trace = false;
+
+       private final Path sourceBasePath;
+       private final Path targetBasePath;
+       private final boolean delete;
+       private final boolean recursive;
+
+       private SyncResult<Path> syncResult = new SyncResult<>();
+
+       public BasicSyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) {
+               this.sourceBasePath = sourceBasePath;
+               this.targetBasePath = targetBasePath;
+               this.delete = delete;
+               this.recursive = recursive;
+       }
+
+       @Override
+       public FileVisitResult preVisitDirectory(Path sourceDir, BasicFileAttributes attrs) throws IOException {
+               if (!recursive && !sourceDir.equals(sourceBasePath))
+                       return FileVisitResult.SKIP_SUBTREE;
+               Path targetDir = toTargetPath(sourceDir);
+               Files.createDirectories(targetDir);
+               return FileVisitResult.CONTINUE;
+       }
+
+       @Override
+       public FileVisitResult postVisitDirectory(Path sourceDir, IOException exc) throws IOException {
+               if (delete) {
+                       Path targetDir = toTargetPath(sourceDir);
+                       for (Path targetPath : Files.newDirectoryStream(targetDir)) {
+                               Path sourcePath = sourceDir.resolve(targetPath.getFileName());
+                               if (!Files.exists(sourcePath)) {
+                                       try {
+                                               FsSyncUtils.delete(targetPath);
+                                               deleted(targetPath);
+                                       } catch (Exception e) {
+                                               deleteFailed(targetPath, exc);
+                                       }
+                               }
+                       }
+               }
+               return FileVisitResult.CONTINUE;
+       }
+
+       @Override
+       public FileVisitResult visitFile(Path sourceFile, BasicFileAttributes attrs) throws IOException {
+               Path targetFile = toTargetPath(sourceFile);
+               try {
+                       if (!Files.exists(targetFile)) {
+                               Files.copy(sourceFile, targetFile);
+                               added(sourceFile, targetFile);
+                       } else {
+                               if (shouldOverwrite(sourceFile, targetFile)) {
+                                       Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING);
+                               }
+                       }
+               } catch (Exception e) {
+                       copyFailed(sourceFile, targetFile, e);
+               }
+               return FileVisitResult.CONTINUE;
+       }
+
+       protected boolean shouldOverwrite(Path sourceFile, Path targetFile) throws IOException {
+               long sourceSize = Files.size(sourceFile);
+               long targetSize = Files.size(targetFile);
+               if (sourceSize != targetSize) {
+                       return true;
+               }
+               FileTime sourceLastModif = Files.getLastModifiedTime(sourceFile);
+               FileTime targetLastModif = Files.getLastModifiedTime(targetFile);
+               if (sourceLastModif.compareTo(targetLastModif) > 0)
+                       return true;
+               return shouldOverwriteLaterSameSize(sourceFile, targetFile);
+       }
+
+       protected boolean shouldOverwriteLaterSameSize(Path sourceFile, Path targetFile) {
+               return false;
+       }
+
+//     @Override
+//     public FileVisitResult visitFileFailed(Path sourceFile, IOException exc) throws IOException {
+//             error("Cannot sync " + sourceFile, exc);
+//             return FileVisitResult.CONTINUE;
+//     }
+
+       private Path toTargetPath(Path sourcePath) {
+               Path relativePath = sourceBasePath.relativize(sourcePath);
+               Path targetPath = targetBasePath.resolve(relativePath.toString());
+               return targetPath;
+       }
+
+       public Path getSourceBasePath() {
+               return sourceBasePath;
+       }
+
+       public Path getTargetBasePath() {
+               return targetBasePath;
+       }
+
+       protected void added(Path sourcePath, Path targetPath) {
+               syncResult.getAdded().add(targetPath);
+               if (isTraceEnabled())
+                       trace("Added " + sourcePath + " as " + targetPath);
+       }
+
+       protected void modified(Path sourcePath, Path targetPath) {
+               syncResult.getModified().add(targetPath);
+               if (isTraceEnabled())
+                       trace("Overwritten from " + sourcePath + " to " + targetPath);
+       }
+
+       protected void copyFailed(Path sourcePath, Path targetPath, Exception e) {
+               syncResult.addError(sourcePath, targetPath, e);
+               if (isTraceEnabled())
+                       error("Cannot copy " + sourcePath + " to " + targetPath, e);
+       }
+
+       protected void deleted(Path targetPath) {
+               syncResult.getDeleted().add(targetPath);
+               if (isTraceEnabled())
+                       trace("Deleted " + targetPath);
+       }
+
+       protected void deleteFailed(Path targetPath, Exception e) {
+               syncResult.addError(null, targetPath, e);
+               if (isTraceEnabled())
+                       error("Cannot delete " + targetPath, e);
+       }
+
+       /** Log error. */
+       protected void error(Object obj, Throwable e) {
+               System.err.println(obj);
+               e.printStackTrace();
+       }
+
+       protected boolean isTraceEnabled() {
+               return trace;
+       }
+
+       protected void trace(Object obj) {
+               System.out.println(obj);
+       }
+
+       public SyncResult<Path> getSyncResult() {
+               return syncResult;
+       }
+
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/FsSyncUtils.java b/org.argeo.cms/src/org/argeo/cms/file/FsSyncUtils.java
new file mode 100644 (file)
index 0000000..68eb5ab
--- /dev/null
@@ -0,0 +1,62 @@
+package org.argeo.cms.file;
+
+import java.io.IOException;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+
+public class FsSyncUtils {
+       /** Sync a source path with a target path. */
+       public static void sync(Path sourceBasePath, Path targetBasePath) {
+               sync(sourceBasePath, targetBasePath, false);
+       }
+
+       /** Sync a source path with a target path. */
+       public static void sync(Path sourceBasePath, Path targetBasePath, boolean delete) {
+               sync(new BasicSyncFileVisitor(sourceBasePath, targetBasePath, delete, true));
+       }
+
+       public static void sync(BasicSyncFileVisitor syncFileVisitor) {
+               try {
+                       Files.walkFileTree(syncFileVisitor.getSourceBasePath(), syncFileVisitor);
+               } catch (Exception e) {
+                       throw new RuntimeException("Cannot sync " + syncFileVisitor.getSourceBasePath() + " with "
+                                       + syncFileVisitor.getTargetBasePath(), e);
+               }
+       }
+
+       /**
+        * Deletes this path, recursively if needed. Does nothing if the path does not
+        * exist.
+        */
+       public static void delete(Path path) {
+               try {
+                       if (!Files.exists(path))
+                               return;
+                       Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
+                               @Override
+                               public FileVisitResult postVisitDirectory(Path directory, IOException e) throws IOException {
+                                       if (e != null)
+                                               throw e;
+                                       Files.delete(directory);
+                                       return FileVisitResult.CONTINUE;
+                               }
+
+                               @Override
+                               public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
+                                       Files.delete(file);
+                                       return FileVisitResult.CONTINUE;
+                               }
+                       });
+               } catch (IOException e) {
+                       throw new RuntimeException("Cannot delete " + path, e);
+               }
+       }
+
+       /** Singleton. */
+       private FsSyncUtils() {
+
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/PathSync.java b/org.argeo.cms/src/org/argeo/cms/file/PathSync.java
new file mode 100644 (file)
index 0000000..dc26aa2
--- /dev/null
@@ -0,0 +1,59 @@
+package org.argeo.cms.file;
+
+import java.net.URI;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.concurrent.Callable;
+
+/** Synchronises two paths. */
+public class PathSync implements Callable<SyncResult<Path>> {
+       private final URI sourceUri, targetUri;
+       private final boolean delete;
+       private final boolean recursive;
+
+       public PathSync(URI sourceUri, URI targetUri) {
+               this(sourceUri, targetUri, false, false);
+       }
+
+       public PathSync(URI sourceUri, URI targetUri, boolean delete, boolean recursive) {
+               this.sourceUri = sourceUri;
+               this.targetUri = targetUri;
+               this.delete = delete;
+               this.recursive = recursive;
+       }
+
+       @Override
+       public SyncResult<Path> call() {
+               try {
+                       Path sourceBasePath = createPath(sourceUri);
+                       Path targetBasePath = createPath(targetUri);
+                       SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath, delete, recursive);
+                       Files.walkFileTree(sourceBasePath, syncFileVisitor);
+                       return syncFileVisitor.getSyncResult();
+               } catch (Exception e) {
+                       throw new IllegalStateException("Cannot sync " + sourceUri + " to " + targetUri, e);
+               }
+       }
+
+       private Path createPath(URI uri) {
+               Path path;
+               if (uri.getScheme() == null) {
+                       path = Paths.get(uri.getPath());
+               } else if (uri.getScheme().equals("file")) {
+                       FileSystemProvider fsProvider = FileSystems.getDefault().provider();
+                       path = fsProvider.getPath(uri);
+               } else if (uri.getScheme().equals("davex")) {
+                       throw new UnsupportedOperationException();
+//                     FileSystemProvider fsProvider = new DavexFsProvider();
+//                     path = fsProvider.getPath(uri);
+//             } else if (uri.getScheme().equals("sftp")) {
+//                     Sftp sftp = new Sftp(uri);
+//                     path = sftp.getBasePath();
+               } else
+                       throw new IllegalArgumentException("URI scheme not supported for " + uri);
+               return path;
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/SyncFileVisitor.java b/org.argeo.cms/src/org/argeo/cms/file/SyncFileVisitor.java
new file mode 100644 (file)
index 0000000..a69d2bf
--- /dev/null
@@ -0,0 +1,30 @@
+package org.argeo.cms.file;
+
+import java.nio.file.Path;
+import java.util.Objects;
+
+import org.argeo.api.cms.CmsLog;
+
+/** Synchronises two directory structures. */
+public class SyncFileVisitor extends BasicSyncFileVisitor {
+       private final static CmsLog log = CmsLog.getLog(SyncFileVisitor.class);
+
+       public SyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) {
+               super(sourceBasePath, targetBasePath, delete, recursive);
+       }
+
+       @Override
+       protected void error(Object obj, Throwable e) {
+               log.error(Objects.toString(obj), e);
+       }
+
+       @Override
+       protected boolean isTraceEnabled() {
+               return log.isTraceEnabled();
+       }
+
+       @Override
+       protected void trace(Object obj) {
+               log.error(Objects.toString(obj));
+       }
+}
diff --git a/org.argeo.cms/src/org/argeo/cms/file/SyncResult.java b/org.argeo.cms/src/org/argeo/cms/file/SyncResult.java
new file mode 100644 (file)
index 0000000..35ab3f9
--- /dev/null
@@ -0,0 +1,101 @@
+package org.argeo.cms.file;
+
+import java.time.Instant;
+import java.util.Set;
+import java.util.TreeSet;
+
+/** Describes what happendend during a sync operation. */
+public class SyncResult<T> {
+       private final Set<T> added = new TreeSet<>();
+       private final Set<T> modified = new TreeSet<>();
+       private final Set<T> deleted = new TreeSet<>();
+       private final Set<Error> errors = new TreeSet<>();
+
+       public Set<T> getAdded() {
+               return added;
+       }
+
+       public Set<T> getModified() {
+               return modified;
+       }
+
+       public Set<T> getDeleted() {
+               return deleted;
+       }
+
+       public Set<Error> getErrors() {
+               return errors;
+       }
+
+       public void addError(T sourcePath, T targetPath, Exception e) {
+               Error error = new Error(sourcePath, targetPath, e);
+               errors.add(error);
+       }
+
+       public boolean noModification() {
+               return modified.isEmpty() && deleted.isEmpty() && added.isEmpty();
+       }
+
+       @Override
+       public String toString() {
+               if (noModification())
+                       return "No modification.";
+               StringBuffer sb = new StringBuffer();
+               for (T p : modified)
+                       sb.append("MOD ").append(p).append('\n');
+               for (T p : deleted)
+                       sb.append("DEL ").append(p).append('\n');
+               for (T p : added)
+                       sb.append("ADD ").append(p).append('\n');
+               for (Error error : errors)
+                       sb.append(error).append('\n');
+               return sb.toString();
+       }
+
+       public class Error implements Comparable<Error> {
+               private final T sourcePath;// if null this is a failed delete
+               private final T targetPath;
+               private final Exception exception;
+               private final Instant timestamp = Instant.now();
+
+               public Error(T sourcePath, T targetPath, Exception e) {
+                       super();
+                       this.sourcePath = sourcePath;
+                       this.targetPath = targetPath;
+                       this.exception = e;
+               }
+
+               public T getSourcePath() {
+                       return sourcePath;
+               }
+
+               public T getTargetPath() {
+                       return targetPath;
+               }
+
+               public Exception getException() {
+                       return exception;
+               }
+
+               public Instant getTimestamp() {
+                       return timestamp;
+               }
+
+               @Override
+               public int compareTo(Error o) {
+                       return timestamp.compareTo(o.timestamp);
+               }
+
+               @Override
+               public int hashCode() {
+                       return timestamp.hashCode();
+               }
+
+               @Override
+               public String toString() {
+                       return "ERR " + timestamp + (sourcePath == null ? "Deletion failed" : "Copy failed " + sourcePath) + " "
+                                       + targetPath + " " + exception.getMessage();
+               }
+
+       }
+}