From a8731626eaf812794bec138649de575e6e036245 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Sat, 11 Jan 2020 13:16:47 +0100 Subject: [PATCH] Introduce multi-commands command line utilities. --- .../src/org/argeo/cms/cli/ArgeoCli.java | 31 +++++ .../org/argeo/cli/CommandArgsException.java | 32 +++++ .../src/org/argeo/cli/CommandsCli.java | 131 ++++++++++++++++++ .../src/org/argeo/cli/DescribedCommand.java | 55 ++++++++ .../src/org/argeo/cli/HelpCommand.java | 129 +++++++++++++++++ .../src/org/argeo/cli/fs/FileSync.java | 102 ++++++++++++++ .../src/org/argeo/cli/fs/FsCommands.java | 18 +++ .../org/argeo/{sync => cli}/fs/PathSync.java | 25 ++-- .../org/argeo/{sync => cli}/fs/SshSync.java | 2 +- .../{sync => cli}/fs/SyncFileVisitor.java | 14 +- .../src/org/argeo/cli/posix/Echo.java | 46 ++++++ .../org/argeo/cli/posix/PosixCommands.java | 21 +++ .../src/org/argeo/sync/cli/Sync.java | 52 ------- .../org/argeo/fs/BasicSyncFileVisitor.java | 51 +++++-- org.argeo.jcr/src/org/argeo/fs/FsUtils.java | 2 +- .../src/org/argeo/sync/SyncException.java | 0 .../src/org/argeo/sync/SyncResult.java | 101 ++++++++++++++ 17 files changed, 730 insertions(+), 82 deletions(-) create mode 100644 org.argeo.cms/src/org/argeo/cms/cli/ArgeoCli.java create mode 100644 org.argeo.core/src/org/argeo/cli/CommandArgsException.java create mode 100644 org.argeo.core/src/org/argeo/cli/CommandsCli.java create mode 100644 org.argeo.core/src/org/argeo/cli/DescribedCommand.java create mode 100644 org.argeo.core/src/org/argeo/cli/HelpCommand.java create mode 100644 org.argeo.core/src/org/argeo/cli/fs/FileSync.java create mode 100644 org.argeo.core/src/org/argeo/cli/fs/FsCommands.java rename org.argeo.core/src/org/argeo/{sync => cli}/fs/PathSync.java (64%) rename org.argeo.core/src/org/argeo/{sync => cli}/fs/SshSync.java (99%) rename org.argeo.core/src/org/argeo/{sync => cli}/fs/SyncFileVisitor.java (67%) create mode 100644 org.argeo.core/src/org/argeo/cli/posix/Echo.java create mode 100644 org.argeo.core/src/org/argeo/cli/posix/PosixCommands.java delete mode 100644 org.argeo.core/src/org/argeo/sync/cli/Sync.java rename {org.argeo.core => org.argeo.jcr}/src/org/argeo/sync/SyncException.java (100%) create mode 100644 org.argeo.jcr/src/org/argeo/sync/SyncResult.java diff --git a/org.argeo.cms/src/org/argeo/cms/cli/ArgeoCli.java b/org.argeo.cms/src/org/argeo/cms/cli/ArgeoCli.java new file mode 100644 index 000000000..5e589229d --- /dev/null +++ b/org.argeo.cms/src/org/argeo/cms/cli/ArgeoCli.java @@ -0,0 +1,31 @@ +package org.argeo.cms.cli; + +import org.apache.commons.cli.Option; +import org.argeo.cli.CommandsCli; +import org.argeo.cli.fs.FsCommands; +import org.argeo.cli.posix.PosixCommands; + +/** Argeo command line tools. */ +public class ArgeoCli extends CommandsCli { + + public ArgeoCli(String commandName) { + super(commandName); + // Common options + options.addOption(Option.builder("v").hasArg().argName("verbose").desc("verbosity").build()); + options.addOption( + Option.builder("D").hasArgs().argName("property=value").desc("use value for given property").build()); + + addCommandsCli(new PosixCommands("posix")); + addCommandsCli(new FsCommands("fs")); + } + + @Override + public String getDescription() { + return "Argeo command line utilities"; + } + + public static void main(String[] args) { + mainImpl(new ArgeoCli("argeo"), args); + } + +} diff --git a/org.argeo.core/src/org/argeo/cli/CommandArgsException.java b/org.argeo.core/src/org/argeo/cli/CommandArgsException.java new file mode 100644 index 000000000..d7a615a8e --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/CommandArgsException.java @@ -0,0 +1,32 @@ +package org.argeo.cli; + +public class CommandArgsException extends IllegalArgumentException { + private static final long serialVersionUID = -7271050747105253935L; + private String commandName; + private volatile CommandsCli commandsCli; + + public CommandArgsException(Exception cause) { + super(cause.getMessage(), cause); + } + + public CommandArgsException(String message) { + super(message); + } + + public String getCommandName() { + return commandName; + } + + public void setCommandName(String commandName) { + this.commandName = commandName; + } + + public CommandsCli getCommandsCli() { + return commandsCli; + } + + public void setCommandsCli(CommandsCli commandsCli) { + this.commandsCli = commandsCli; + } + +} diff --git a/org.argeo.core/src/org/argeo/cli/CommandsCli.java b/org.argeo.core/src/org/argeo/cli/CommandsCli.java new file mode 100644 index 000000000..b0879f0a4 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/CommandsCli.java @@ -0,0 +1,131 @@ +package org.argeo.cli; + +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.function.Function; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** Base class for a CLI managing sub commands. */ +public abstract class CommandsCli implements DescribedCommand { + public final static String HELP = "help"; + + private final String commandName; + private Map, ?>> commands = new TreeMap<>(); + + protected final Options options = new Options(); + + public CommandsCli(String commandName) { + this.commandName = commandName; + } + + @Override + public Object apply(List args) { + String cmd = null; + List newArgs = new ArrayList<>(); + try { + CommandLineParser clParser = new DefaultParser(); + CommandLine commonCl = clParser.parse(getOptions(), args.toArray(new String[args.size()]), true); + List leftOvers = commonCl.getArgList(); + for (String arg : leftOvers) { + if (!arg.startsWith("-") && cmd == null) { + cmd = arg; + } else { + newArgs.add(arg); + } + } + } catch (ParseException e) { + CommandArgsException cae = new CommandArgsException(e); + throw cae; + } + + Function, ?> function = cmd != null ? getCommand(cmd) : getDefaultCommand(); + if (function == null) + throw new IllegalArgumentException("Uknown command " + cmd); + try { + return function.apply(newArgs).toString(); + } catch (CommandArgsException e) { + if (e.getCommandName() == null) { + e.setCommandName(cmd); + e.setCommandsCli(this); + } + throw e; + } catch (IllegalArgumentException e) { + CommandArgsException cae = new CommandArgsException(e); + cae.setCommandName(cmd); + throw cae; + } + } + + @Override + public Options getOptions() { + return options; + } + + protected void addCommand(String cmd, Function, ?> function) { + commands.put(cmd, function); + + } + + @Override + public String getUsage() { + return "[command]"; + } + + protected void addCommandsCli(CommandsCli commandsCli) { + addCommand(commandsCli.getCommandName(), commandsCli); + commandsCli.addCommand(HELP, new HelpCommand(this, commandsCli)); + } + + public String getCommandName() { + return commandName; + } + + public Set getSubCommands() { + return commands.keySet(); + } + + public Function, ?> getCommand(String command) { + return commands.get(command); + } + + public HelpCommand getHelpCommand() { + return (HelpCommand) getCommand(HELP); + } + + public Function, String> getDefaultCommand() { + return getHelpCommand(); + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(CommandsCli cli, String[] args) { + try { + cli.addCommand(CommandsCli.HELP, new HelpCommand(null, cli)); + Object output = cli.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (CommandArgsException e) { + System.err.println("Wrong arguments " + Arrays.toString(args) + ": " + e.getMessage()); + if (e.getCommandName() != null) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(e.getCommandsCli(), e.getCommandName(), out); + System.err.println(out.toString()); + } else { + e.printStackTrace(); + } + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } +} diff --git a/org.argeo.core/src/org/argeo/cli/DescribedCommand.java b/org.argeo.core/src/org/argeo/cli/DescribedCommand.java new file mode 100644 index 000000000..9587206b8 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/DescribedCommand.java @@ -0,0 +1,55 @@ +package org.argeo.cli; + +import java.io.StringWriter; +import java.util.Arrays; +import java.util.List; +import java.util.function.Function; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +/** A command that can be described. */ +public interface DescribedCommand extends Function, T> { + default Options getOptions() { + return new Options(); + } + + String getDescription(); + + default String getUsage() { + return null; + } + + default String getExamples() { + return null; + } + + default CommandLine toCommandLine(List args) { + try { + DefaultParser parser = new DefaultParser(); + return parser.parse(getOptions(), args.toArray(new String[args.size()])); + } catch (ParseException e) { + throw new CommandArgsException(e); + } + } + + /** In order to implement quickly a main method. */ + public static void mainImpl(DescribedCommand command, String[] args) { + try { + Object output = command.apply(Arrays.asList(args)); + System.out.println(output); + System.exit(0); + } catch (IllegalArgumentException e) { + StringWriter out = new StringWriter(); + HelpCommand.printHelp(command, out); + System.err.println(out.toString()); + System.exit(1); + } catch (Exception e) { + e.printStackTrace(); + System.exit(1); + } + } + +} diff --git a/org.argeo.core/src/org/argeo/cli/HelpCommand.java b/org.argeo.core/src/org/argeo/cli/HelpCommand.java new file mode 100644 index 000000000..067aee855 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/HelpCommand.java @@ -0,0 +1,129 @@ +package org.argeo.cli; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.List; +import java.util.function.Function; + +import org.apache.commons.cli.HelpFormatter; +import org.apache.commons.cli.Options; + +/** A special command that can describe {@link DescribedCommand}. */ +public class HelpCommand implements DescribedCommand { + private CommandsCli commandsCli; + private CommandsCli parentCommandsCli; + + // Help formatting + private static int helpWidth = 80; + private static int helpLeftPad = 4; + private static int helpDescPad = 20; + + public HelpCommand(CommandsCli parentCommandsCli, CommandsCli commandsCli) { + super(); + this.parentCommandsCli = parentCommandsCli; + this.commandsCli = commandsCli; + } + + @Override + public String apply(List args) { + StringWriter out = new StringWriter(); + + if (args.size() == 0) {// overview + printHelp(commandsCli, out); + } else { + String cmd = args.get(0); + Function, ?> function = commandsCli.getCommand(cmd); + if (function == null) + return "Command " + cmd + " not found."; + Options options; + String examples; + DescribedCommand command = null; + if (function instanceof DescribedCommand) { + command = (DescribedCommand) function; + options = command.getOptions(); + examples = command.getExamples(); + } else { + options = new Options(); + examples = null; + } + String description = getShortDescription(function); + String commandCall = getCommandUsage(cmd, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, commandCall, description, options, helpLeftPad, + helpDescPad, examples, false); + } + return out.toString(); + } + + private static String getShortDescription(Function, ?> function) { + if (function instanceof DescribedCommand) { + return ((DescribedCommand) function).getDescription(); + } else { + return function.toString(); + } + } + + public String getCommandUsage(String cmd, DescribedCommand command) { + String commandCall = getCommandCall(commandsCli) + " " + cmd; + assert command != null; + if (command != null && command.getUsage() != null) { + commandCall = commandCall + " " + command.getUsage(); + } + return commandCall; + } + + @Override + public String getDescription() { + return "Shows this help or describes a command"; + } + + @Override + public String getUsage() { + return "[command]"; + } + + public CommandsCli getParentCommandsCli() { + return parentCommandsCli; + } + + protected String getCommandCall(CommandsCli commandsCli) { + HelpCommand hc = commandsCli.getHelpCommand(); + if (hc.getParentCommandsCli() != null) { + return getCommandCall(hc.getParentCommandsCli()) + " " + commandsCli.getCommandName(); + } else { + return commandsCli.getCommandName(); + } + } + + public static void printHelp(DescribedCommand command, StringWriter out) { + String usage = "java " + command.getClass().getName() + (command.getUsage() != null ? " " + command.getUsage() : ""); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), helpLeftPad, + helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, String commandName, StringWriter out) { + DescribedCommand command = (DescribedCommand) commandsCli.getCommand(commandName); + String usage = commandsCli.getHelpCommand().getCommandUsage(commandName, command); + HelpFormatter formatter = new HelpFormatter(); + formatter.printHelp(new PrintWriter(out), helpWidth, usage, command.getDescription(), command.getOptions(), helpLeftPad, + helpDescPad, command.getExamples(), false); + + } + + public static void printHelp(CommandsCli commandsCli, StringWriter out) { + out.append(commandsCli.getDescription()).append('\n'); + String leftPad = " ".repeat(helpLeftPad); + for (String cmd : commandsCli.getSubCommands()) { + Function, ?> function = commandsCli.getCommand(cmd); + assert function != null; + out.append(leftPad); + out.append(cmd); + // FIXME deal with long commands + out.append(" ".repeat(helpDescPad - cmd.length())); + out.append(getShortDescription(function)); + out.append('\n'); + } + } +} diff --git a/org.argeo.core/src/org/argeo/cli/fs/FileSync.java b/org.argeo.core/src/org/argeo/cli/fs/FileSync.java new file mode 100644 index 000000000..ba529eae2 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/fs/FileSync.java @@ -0,0 +1,102 @@ +package org.argeo.cli.fs; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.argeo.cli.CommandArgsException; +import org.argeo.cli.DescribedCommand; +import org.argeo.sync.SyncResult; + +public class FileSync implements DescribedCommand> { + final static Option deleteOption = Option.builder().longOpt("delete").desc("delete from target").build(); + final static Option recursiveOption = Option.builder("r").longOpt("recursive").desc("recurse into directories") + .build(); + final static Option progressOption = Option.builder().longOpt("progress").hasArg(false).desc("show progress") + .build(); + + @Override + public SyncResult apply(List t) { + try { + CommandLine line = toCommandLine(t); + List remaining = line.getArgList(); + if (remaining.size() == 0) { + throw new CommandArgsException("There must be at least one argument"); + } + URI sourceUri = new URI(remaining.get(0)); + URI targetUri; + if (remaining.size() == 1) { + targetUri = Paths.get(System.getProperty("user.dir")).toUri(); + } else { + targetUri = new URI(remaining.get(1)); + } + boolean delete = line.hasOption(deleteOption.getLongOpt()); + boolean recursive = line.hasOption(recursiveOption.getLongOpt()); + PathSync pathSync = new PathSync(sourceUri, targetUri, delete, recursive); + return pathSync.call(); + } catch (URISyntaxException e) { + throw new CommandArgsException(e); + } + } + + @Override + public Options getOptions() { + Options options = new Options(); + options.addOption(recursiveOption); + options.addOption(deleteOption); + options.addOption(progressOption); + return options; + } + + @Override + public String getUsage() { + return "[source URI] [target URI]"; + } + + public static void main(String[] args) { + DescribedCommand.mainImpl(new FileSync(), args); +// Options options = new Options(); +// options.addOption("r", "recursive", false, "recurse into directories"); +// options.addOption(Option.builder().longOpt("progress").hasArg(false).desc("show progress").build()); +// +// CommandLineParser parser = new DefaultParser(); +// try { +// CommandLine line = parser.parse(options, args); +// List remaining = line.getArgList(); +// if (remaining.size() == 0) { +// System.err.println("There must be at least one argument"); +// printHelp(options); +// System.exit(1); +// } +// URI sourceUri = new URI(remaining.get(0)); +// URI targetUri; +// if (remaining.size() == 1) { +// targetUri = Paths.get(System.getProperty("user.dir")).toUri(); +// } else { +// targetUri = new URI(remaining.get(1)); +// } +// PathSync pathSync = new PathSync(sourceUri, targetUri); +// pathSync.run(); +// } catch (Exception exp) { +// exp.printStackTrace(); +// printHelp(options); +// System.exit(1); +// } + } + +// public static void printHelp(Options options) { +// HelpFormatter formatter = new HelpFormatter(); +// formatter.printHelp("sync SRC [DEST]", options, true); +// } + + @Override + public String getDescription() { + return "Synchronises files"; + } + +} diff --git a/org.argeo.core/src/org/argeo/cli/fs/FsCommands.java b/org.argeo.core/src/org/argeo/cli/fs/FsCommands.java new file mode 100644 index 000000000..c08ad0091 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/fs/FsCommands.java @@ -0,0 +1,18 @@ +package org.argeo.cli.fs; + +import org.argeo.cli.CommandsCli; + +/** File utilities. */ +public class FsCommands extends CommandsCli { + + public FsCommands(String commandName) { + super(commandName); + addCommand("sync", new FileSync()); + } + + @Override + public String getDescription() { + return "Utilities around files and file systems"; + } + +} diff --git a/org.argeo.core/src/org/argeo/sync/fs/PathSync.java b/org.argeo.core/src/org/argeo/cli/fs/PathSync.java similarity index 64% rename from org.argeo.core/src/org/argeo/sync/fs/PathSync.java rename to org.argeo.core/src/org/argeo/cli/fs/PathSync.java index 151194da6..01cef9ee0 100644 --- a/org.argeo.core/src/org/argeo/sync/fs/PathSync.java +++ b/org.argeo.core/src/org/argeo/cli/fs/PathSync.java @@ -1,4 +1,4 @@ -package org.argeo.sync.fs; +package org.argeo.cli.fs; import java.net.URI; import java.nio.file.FileSystems; @@ -6,30 +6,39 @@ 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; import org.argeo.jackrabbit.fs.DavexFsProvider; import org.argeo.ssh.Sftp; -import org.argeo.sync.SyncException; +import org.argeo.sync.SyncResult; /** Synchronises two paths. */ -public class PathSync implements Runnable { +public class PathSync implements Callable> { private final URI sourceUri, targetUri; - private boolean delete = false; + 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 void run() { + public SyncResult call() { try { Path sourceBasePath = createPath(sourceUri); Path targetBasePath = createPath(targetUri); - SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath, delete); + SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath, delete, recursive); Files.walkFileTree(sourceBasePath, syncFileVisitor); + return syncFileVisitor.getSyncResult(); } catch (Exception e) { - e.printStackTrace(); + throw new IllegalStateException("Cannot sync " + sourceUri + " to " + targetUri, e); } } @@ -47,7 +56,7 @@ public class PathSync implements Runnable { Sftp sftp = new Sftp(uri); path = sftp.getBasePath(); } else - throw new SyncException("URI scheme not supported for " + uri); + throw new IllegalArgumentException("URI scheme not supported for " + uri); return path; } } diff --git a/org.argeo.core/src/org/argeo/sync/fs/SshSync.java b/org.argeo.core/src/org/argeo/cli/fs/SshSync.java similarity index 99% rename from org.argeo.core/src/org/argeo/sync/fs/SshSync.java rename to org.argeo.core/src/org/argeo/cli/fs/SshSync.java index 9b15a32ea..268e7d899 100644 --- a/org.argeo.core/src/org/argeo/sync/fs/SshSync.java +++ b/org.argeo.core/src/org/argeo/cli/fs/SshSync.java @@ -1,4 +1,4 @@ -package org.argeo.sync.fs; +package org.argeo.cli.fs; import java.io.IOException; import java.io.InputStream; diff --git a/org.argeo.core/src/org/argeo/sync/fs/SyncFileVisitor.java b/org.argeo.core/src/org/argeo/cli/fs/SyncFileVisitor.java similarity index 67% rename from org.argeo.core/src/org/argeo/sync/fs/SyncFileVisitor.java rename to org.argeo.core/src/org/argeo/cli/fs/SyncFileVisitor.java index d59a611fc..892df5060 100644 --- a/org.argeo.core/src/org/argeo/sync/fs/SyncFileVisitor.java +++ b/org.argeo.core/src/org/argeo/cli/fs/SyncFileVisitor.java @@ -1,4 +1,4 @@ -package org.argeo.sync.fs; +package org.argeo.cli.fs; import java.nio.file.Path; @@ -10,8 +10,8 @@ import org.argeo.fs.BasicSyncFileVisitor; public class SyncFileVisitor extends BasicSyncFileVisitor { private final static Log log = LogFactory.getLog(SyncFileVisitor.class); - public SyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete) { - super(sourceBasePath, targetBasePath, delete); + public SyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete, boolean recursive) { + super(sourceBasePath, targetBasePath, delete, recursive); } @Override @@ -20,12 +20,12 @@ public class SyncFileVisitor extends BasicSyncFileVisitor { } @Override - protected boolean isDebugEnabled() { - return log.isDebugEnabled(); + protected boolean isTraceEnabled() { + return log.isTraceEnabled(); } @Override - protected void debug(Object obj) { - log.debug(obj); + protected void trace(Object obj) { + log.trace(obj); } } diff --git a/org.argeo.core/src/org/argeo/cli/posix/Echo.java b/org.argeo.core/src/org/argeo/cli/posix/Echo.java new file mode 100644 index 000000000..5746ebd0f --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/posix/Echo.java @@ -0,0 +1,46 @@ +package org.argeo.cli.posix; + +import java.util.List; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.Option; +import org.apache.commons.cli.Options; +import org.argeo.cli.DescribedCommand; + +public class Echo implements DescribedCommand { + + @Override + public Options getOptions() { + Options options = new Options(); + options.addOption(Option.builder("n").desc("do not output the trailing newline").build()); + return options; + } + + @Override + public String getDescription() { + return "Display a line of text"; + } + + @Override + public String getUsage() { + return "[STRING]..."; + } + + @Override + public String apply(List args) { + CommandLine cl = toCommandLine(args); + + StringBuffer sb = new StringBuffer(); + for (String s : cl.getArgList()) { + sb.append(s).append(' '); + } + + if (cl.hasOption('n')) { + sb.deleteCharAt(sb.length() - 1); + } else { + sb.setCharAt(sb.length() - 1, '\n'); + } + return sb.toString(); + } + +} diff --git a/org.argeo.core/src/org/argeo/cli/posix/PosixCommands.java b/org.argeo.core/src/org/argeo/cli/posix/PosixCommands.java new file mode 100644 index 000000000..bb6af67b8 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/posix/PosixCommands.java @@ -0,0 +1,21 @@ +package org.argeo.cli.posix; + +import org.argeo.cli.CommandsCli; + +/** POSIX commands. */ +public class PosixCommands extends CommandsCli { + + public PosixCommands(String commandName) { + super(commandName); + addCommand("echo", new Echo()); + } + + @Override + public String getDescription() { + return "Reimplementation of some POSIX commands in plain Java"; + } + + public static void main(String[] args) { + mainImpl(new PosixCommands("argeo-posix"), args); + } +} diff --git a/org.argeo.core/src/org/argeo/sync/cli/Sync.java b/org.argeo.core/src/org/argeo/sync/cli/Sync.java deleted file mode 100644 index d21b5a255..000000000 --- a/org.argeo.core/src/org/argeo/sync/cli/Sync.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.argeo.sync.cli; - -import java.net.URI; -import java.nio.file.Paths; -import java.util.List; - -import org.apache.commons.cli.CommandLine; -import org.apache.commons.cli.CommandLineParser; -import org.apache.commons.cli.DefaultParser; -import org.apache.commons.cli.HelpFormatter; -import org.apache.commons.cli.Option; -import org.apache.commons.cli.Options; -import org.argeo.sync.fs.PathSync; - -public class Sync { - - public static void main(String[] args) { - Options options = new Options(); - options.addOption("r", "recursive", false, "recurse into directories"); - options.addOption(Option.builder().longOpt("progress").hasArg(false).desc("show progress").build()); - - CommandLineParser parser = new DefaultParser(); - try { - CommandLine line = parser.parse(options, args); - List remaining = line.getArgList(); - if (remaining.size() == 0) { - System.err.println("There must be at least one argument"); - printHelp(options); - System.exit(1); - } - URI sourceUri = new URI(remaining.get(0)); - URI targetUri; - if (remaining.size() == 1) { - targetUri = Paths.get(System.getProperty("user.dir")).toUri(); - } else { - targetUri = new URI(remaining.get(1)); - } - PathSync pathSync = new PathSync(sourceUri, targetUri); - pathSync.run(); - } catch (Exception exp) { - exp.printStackTrace(); - printHelp(options); - System.exit(1); - } - } - - public static void printHelp(Options options) { - HelpFormatter formatter = new HelpFormatter(); - formatter.printHelp("sync SRC [DEST]", options, true); - } - -} diff --git a/org.argeo.jcr/src/org/argeo/fs/BasicSyncFileVisitor.java b/org.argeo.jcr/src/org/argeo/fs/BasicSyncFileVisitor.java index c60492d08..03bac592c 100644 --- a/org.argeo.jcr/src/org/argeo/fs/BasicSyncFileVisitor.java +++ b/org.argeo.jcr/src/org/argeo/fs/BasicSyncFileVisitor.java @@ -9,23 +9,31 @@ import java.nio.file.StandardCopyOption; import java.nio.file.attribute.BasicFileAttributes; import java.nio.file.attribute.FileTime; +import org.argeo.sync.SyncResult; + /** Synchronises two directory structures. */ public class BasicSyncFileVisitor extends SimpleFileVisitor { // TODO make it configurable - private boolean debug = false; + private boolean trace = false; private final Path sourceBasePath; private final Path targetBasePath; private final boolean delete; + private final boolean recursive; + + private SyncResult syncResult = new SyncResult<>(); - public BasicSyncFileVisitor(Path sourceBasePath, Path targetBasePath, boolean delete) { + 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; @@ -56,7 +64,7 @@ public class BasicSyncFileVisitor extends SimpleFileVisitor { try { if (!Files.exists(targetFile)) { Files.copy(sourceFile, targetFile); - copied(sourceFile, targetFile); + added(sourceFile, targetFile); } else { if (shouldOverwrite(sourceFile, targetFile)) { Files.copy(sourceFile, targetFile, StandardCopyOption.REPLACE_EXISTING); @@ -105,22 +113,34 @@ public class BasicSyncFileVisitor extends SimpleFileVisitor { return targetBasePath; } - protected void copied(Path sourcePath, Path targetPath) { - if (isDebugEnabled()) - debug("Copied " + sourcePath + " to " + targetPath); + 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) { - error("Cannot copy " + sourcePath + " to " + targetPath, e); + syncResult.addError(sourcePath, targetPath, e); + if (isTraceEnabled()) + error("Cannot copy " + sourcePath + " to " + targetPath, e); } protected void deleted(Path targetPath) { - if (isDebugEnabled()) - debug("Deleted " + targetPath); + syncResult.getDeleted().add(targetPath); + if (isTraceEnabled()) + trace("Deleted " + targetPath); } protected void deleteFailed(Path targetPath, Exception e) { - error("Cannot delete " + targetPath, e); + syncResult.addError(null, targetPath, e); + if (isTraceEnabled()) + error("Cannot delete " + targetPath, e); } /** Log error. */ @@ -129,11 +149,16 @@ public class BasicSyncFileVisitor extends SimpleFileVisitor { e.printStackTrace(); } - protected boolean isDebugEnabled() { - return debug; + protected boolean isTraceEnabled() { + return trace; } - protected void debug(Object obj) { + protected void trace(Object obj) { System.out.println(obj); } + + public SyncResult getSyncResult() { + return syncResult; + } + } diff --git a/org.argeo.jcr/src/org/argeo/fs/FsUtils.java b/org.argeo.jcr/src/org/argeo/fs/FsUtils.java index 6fc7bd25d..c96f56ed2 100644 --- a/org.argeo.jcr/src/org/argeo/fs/FsUtils.java +++ b/org.argeo.jcr/src/org/argeo/fs/FsUtils.java @@ -16,7 +16,7 @@ public class FsUtils { /** Sync a source path with a target path. */ public static void sync(Path sourceBasePath, Path targetBasePath, boolean delete) { - sync(new BasicSyncFileVisitor(sourceBasePath, targetBasePath, delete)); + sync(new BasicSyncFileVisitor(sourceBasePath, targetBasePath, delete, true)); } public static void sync(BasicSyncFileVisitor syncFileVisitor) { diff --git a/org.argeo.core/src/org/argeo/sync/SyncException.java b/org.argeo.jcr/src/org/argeo/sync/SyncException.java similarity index 100% rename from org.argeo.core/src/org/argeo/sync/SyncException.java rename to org.argeo.jcr/src/org/argeo/sync/SyncException.java diff --git a/org.argeo.jcr/src/org/argeo/sync/SyncResult.java b/org.argeo.jcr/src/org/argeo/sync/SyncResult.java new file mode 100644 index 000000000..6d12ada4a --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/sync/SyncResult.java @@ -0,0 +1,101 @@ +package org.argeo.sync; + +import java.time.Instant; +import java.util.Set; +import java.util.TreeSet; + +/** Describes what happendend during a sync operation. */ +public class SyncResult { + private final Set added = new TreeSet<>(); + private final Set modified = new TreeSet<>(); + private final Set deleted = new TreeSet<>(); + private final Set errors = new TreeSet<>(); + + public Set getAdded() { + return added; + } + + public Set getModified() { + return modified; + } + + public Set getDeleted() { + return deleted; + } + + public Set 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 { + 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(); + } + + } +} -- 2.30.2