From: Mathieu Baudier Date: Sat, 11 Jan 2020 12:16:47 +0000 (+0100) Subject: Introduce multi-commands command line utilities. X-Git-Tag: argeo-commons-2.1.85~35 X-Git-Url: http://git.argeo.org/?p=lgpl%2Fargeo-commons.git;a=commitdiff_plain;h=a8731626eaf812794bec138649de575e6e036245 Introduce multi-commands command line utilities. --- 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/cli/fs/PathSync.java b/org.argeo.core/src/org/argeo/cli/fs/PathSync.java new file mode 100644 index 000000000..01cef9ee0 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/fs/PathSync.java @@ -0,0 +1,62 @@ +package org.argeo.cli.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; + +import org.argeo.jackrabbit.fs.DavexFsProvider; +import org.argeo.ssh.Sftp; +import org.argeo.sync.SyncResult; + +/** Synchronises two paths. */ +public class PathSync implements Callable> { + 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 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 static 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")) { + 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.core/src/org/argeo/cli/fs/SshSync.java b/org.argeo.core/src/org/argeo/cli/fs/SshSync.java new file mode 100644 index 000000000..268e7d899 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/fs/SshSync.java @@ -0,0 +1,135 @@ +package org.argeo.cli.fs; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.DirectoryStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Map; +import java.util.Scanner; + +import org.apache.commons.io.IOUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.apache.sshd.agent.SshAgent; +import org.apache.sshd.agent.SshAgentFactory; +import org.apache.sshd.agent.local.LocalAgentFactory; +import org.apache.sshd.agent.unix.UnixAgentFactory; +import org.apache.sshd.client.SshClient; +import org.apache.sshd.client.future.ConnectFuture; +import org.apache.sshd.client.session.ClientSession; +import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystem; +import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystemProvider; + +public class SshSync { + private final static Log log = LogFactory.getLog(SshSync.class); + + public static void main(String[] args) { + + try (SshClient client = SshClient.setUpDefaultClient()) { + client.start(); + boolean osAgent = true; + SshAgentFactory agentFactory = osAgent ? new UnixAgentFactory() : new LocalAgentFactory(); + // SshAgentFactory agentFactory = new LocalAgentFactory(); + client.setAgentFactory(agentFactory); + SshAgent sshAgent = agentFactory.createClient(client); + + String login = System.getProperty("user.name"); + String host = "localhost"; + int port = 22; + + if (!osAgent) { + String keyPath = "/home/" + login + "/.ssh/id_rsa"; + System.out.print(keyPath + ": "); + Scanner s = new Scanner(System.in); + String password = s.next(); +// KeyPair keyPair = ClientIdentityLoader.DEFAULT.loadClientIdentity(keyPath, +// FilePasswordProvider.of(password)); +// sshAgent.addIdentity(keyPair, "NO COMMENT"); + } + +// List> identities = sshAgent.getIdentities(); +// for (Map.Entry entry : identities) { +// System.out.println(entry.getValue() + " : " + entry.getKey()); +// } + + ConnectFuture connectFuture = client.connect(login, host, port); + connectFuture.await(); + ClientSession session = connectFuture.getSession(); + + try { + +// session.addPasswordIdentity(new String(password)); + session.auth().verify(1000l); + + SftpFileSystemProvider fsProvider = new SftpFileSystemProvider(client); + + SftpFileSystem fs = fsProvider.newFileSystem(session); + Path testPath = fs.getPath("/home/" + login + "/tmp"); + Files.list(testPath).forEach(System.out::println); + test(testPath); + + } finally { + client.stop(); + } + } catch (Exception e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + static void test(Path testBase) { + try { + Path testPath = testBase.resolve("ssh-test.txt"); + Files.createFile(testPath); + log.debug("Created file " + testPath); + Files.delete(testPath); + log.debug("Deleted " + testPath); + String txt = "TEST\nTEST2\n"; + byte[] arr = txt.getBytes(); + Files.write(testPath, arr); + log.debug("Wrote " + testPath); + byte[] read = Files.readAllBytes(testPath); + log.debug("Read " + testPath); + Path testDir = testBase.resolve("testDir"); + log.debug("Resolved " + testDir); + // Copy + Files.createDirectory(testDir); + log.debug("Created directory " + testDir); + Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir")); + log.debug("Created sub directories " + subsubdir); + Path copiedFile = testDir.resolve("copiedFile.txt"); + log.debug("Resolved " + copiedFile); + Path relativeCopiedFile = testDir.relativize(copiedFile); + log.debug("Relative copied file " + relativeCopiedFile); + try (OutputStream out = Files.newOutputStream(copiedFile); + InputStream in = Files.newInputStream(testPath)) { + IOUtils.copy(in, out); + } + log.debug("Copied " + testPath + " to " + copiedFile); + Files.delete(testPath); + log.debug("Deleted " + testPath); + byte[] copiedRead = Files.readAllBytes(copiedFile); + log.debug("Read " + copiedFile); + // Browse directories + DirectoryStream files = Files.newDirectoryStream(testDir); + int fileCount = 0; + Path listedFile = null; + for (Path file : files) { + fileCount++; + if (!Files.isDirectory(file)) + listedFile = file; + } + log.debug("Listed " + testDir); + // Generic attributes + Map attrs = Files.readAttributes(copiedFile, "*"); + log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + + } + +} diff --git a/org.argeo.core/src/org/argeo/cli/fs/SyncFileVisitor.java b/org.argeo.core/src/org/argeo/cli/fs/SyncFileVisitor.java new file mode 100644 index 000000000..892df5060 --- /dev/null +++ b/org.argeo.core/src/org/argeo/cli/fs/SyncFileVisitor.java @@ -0,0 +1,31 @@ +package org.argeo.cli.fs; + +import java.nio.file.Path; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.argeo.fs.BasicSyncFileVisitor; + +/** Synchronises two directory structures. */ +public class SyncFileVisitor extends BasicSyncFileVisitor { + private final static Log log = LogFactory.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(obj, e); + } + + @Override + protected boolean isTraceEnabled() { + return log.isTraceEnabled(); + } + + @Override + 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/SyncException.java b/org.argeo.core/src/org/argeo/sync/SyncException.java deleted file mode 100644 index 89bf869a2..000000000 --- a/org.argeo.core/src/org/argeo/sync/SyncException.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.argeo.sync; - -/** Commons exception for sync */ -public class SyncException extends RuntimeException { - private static final long serialVersionUID = -3371314343580218538L; - - public SyncException(String message) { - super(message); - } - - public SyncException(String message, Throwable cause) { - super(message, cause); - } - - public SyncException(Object source, Object target, Throwable cause) { - super("Cannot sync from " + source + " to " + target, cause); - } -} 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.core/src/org/argeo/sync/fs/PathSync.java b/org.argeo.core/src/org/argeo/sync/fs/PathSync.java deleted file mode 100644 index 151194da6..000000000 --- a/org.argeo.core/src/org/argeo/sync/fs/PathSync.java +++ /dev/null @@ -1,53 +0,0 @@ -package org.argeo.sync.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 org.argeo.jackrabbit.fs.DavexFsProvider; -import org.argeo.ssh.Sftp; -import org.argeo.sync.SyncException; - -/** Synchronises two paths. */ -public class PathSync implements Runnable { - private final URI sourceUri, targetUri; - private boolean delete = false; - - public PathSync(URI sourceUri, URI targetUri) { - this.sourceUri = sourceUri; - this.targetUri = targetUri; - } - - @Override - public void run() { - try { - Path sourceBasePath = createPath(sourceUri); - Path targetBasePath = createPath(targetUri); - SyncFileVisitor syncFileVisitor = new SyncFileVisitor(sourceBasePath, targetBasePath, delete); - Files.walkFileTree(sourceBasePath, syncFileVisitor); - } catch (Exception e) { - e.printStackTrace(); - } - } - - private static 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")) { - 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 SyncException("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/sync/fs/SshSync.java deleted file mode 100644 index 9b15a32ea..000000000 --- a/org.argeo.core/src/org/argeo/sync/fs/SshSync.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.argeo.sync.fs; - -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.DirectoryStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Map; -import java.util.Scanner; - -import org.apache.commons.io.IOUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.apache.sshd.agent.SshAgent; -import org.apache.sshd.agent.SshAgentFactory; -import org.apache.sshd.agent.local.LocalAgentFactory; -import org.apache.sshd.agent.unix.UnixAgentFactory; -import org.apache.sshd.client.SshClient; -import org.apache.sshd.client.future.ConnectFuture; -import org.apache.sshd.client.session.ClientSession; -import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystem; -import org.apache.sshd.client.subsystem.sftp.fs.SftpFileSystemProvider; - -public class SshSync { - private final static Log log = LogFactory.getLog(SshSync.class); - - public static void main(String[] args) { - - try (SshClient client = SshClient.setUpDefaultClient()) { - client.start(); - boolean osAgent = true; - SshAgentFactory agentFactory = osAgent ? new UnixAgentFactory() : new LocalAgentFactory(); - // SshAgentFactory agentFactory = new LocalAgentFactory(); - client.setAgentFactory(agentFactory); - SshAgent sshAgent = agentFactory.createClient(client); - - String login = System.getProperty("user.name"); - String host = "localhost"; - int port = 22; - - if (!osAgent) { - String keyPath = "/home/" + login + "/.ssh/id_rsa"; - System.out.print(keyPath + ": "); - Scanner s = new Scanner(System.in); - String password = s.next(); -// KeyPair keyPair = ClientIdentityLoader.DEFAULT.loadClientIdentity(keyPath, -// FilePasswordProvider.of(password)); -// sshAgent.addIdentity(keyPair, "NO COMMENT"); - } - -// List> identities = sshAgent.getIdentities(); -// for (Map.Entry entry : identities) { -// System.out.println(entry.getValue() + " : " + entry.getKey()); -// } - - ConnectFuture connectFuture = client.connect(login, host, port); - connectFuture.await(); - ClientSession session = connectFuture.getSession(); - - try { - -// session.addPasswordIdentity(new String(password)); - session.auth().verify(1000l); - - SftpFileSystemProvider fsProvider = new SftpFileSystemProvider(client); - - SftpFileSystem fs = fsProvider.newFileSystem(session); - Path testPath = fs.getPath("/home/" + login + "/tmp"); - Files.list(testPath).forEach(System.out::println); - test(testPath); - - } finally { - client.stop(); - } - } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - } - - static void test(Path testBase) { - try { - Path testPath = testBase.resolve("ssh-test.txt"); - Files.createFile(testPath); - log.debug("Created file " + testPath); - Files.delete(testPath); - log.debug("Deleted " + testPath); - String txt = "TEST\nTEST2\n"; - byte[] arr = txt.getBytes(); - Files.write(testPath, arr); - log.debug("Wrote " + testPath); - byte[] read = Files.readAllBytes(testPath); - log.debug("Read " + testPath); - Path testDir = testBase.resolve("testDir"); - log.debug("Resolved " + testDir); - // Copy - Files.createDirectory(testDir); - log.debug("Created directory " + testDir); - Path subsubdir = Files.createDirectories(testDir.resolve("subdir/subsubdir")); - log.debug("Created sub directories " + subsubdir); - Path copiedFile = testDir.resolve("copiedFile.txt"); - log.debug("Resolved " + copiedFile); - Path relativeCopiedFile = testDir.relativize(copiedFile); - log.debug("Relative copied file " + relativeCopiedFile); - try (OutputStream out = Files.newOutputStream(copiedFile); - InputStream in = Files.newInputStream(testPath)) { - IOUtils.copy(in, out); - } - log.debug("Copied " + testPath + " to " + copiedFile); - Files.delete(testPath); - log.debug("Deleted " + testPath); - byte[] copiedRead = Files.readAllBytes(copiedFile); - log.debug("Read " + copiedFile); - // Browse directories - DirectoryStream files = Files.newDirectoryStream(testDir); - int fileCount = 0; - Path listedFile = null; - for (Path file : files) { - fileCount++; - if (!Files.isDirectory(file)) - listedFile = file; - } - log.debug("Listed " + testDir); - // Generic attributes - Map attrs = Files.readAttributes(copiedFile, "*"); - log.debug("Read attributes of " + copiedFile + ": " + attrs.keySet()); - } catch (IOException e) { - // TODO Auto-generated catch block - e.printStackTrace(); - } - - } - -} diff --git a/org.argeo.core/src/org/argeo/sync/fs/SyncFileVisitor.java b/org.argeo.core/src/org/argeo/sync/fs/SyncFileVisitor.java deleted file mode 100644 index d59a611fc..000000000 --- a/org.argeo.core/src/org/argeo/sync/fs/SyncFileVisitor.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.argeo.sync.fs; - -import java.nio.file.Path; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.argeo.fs.BasicSyncFileVisitor; - -/** Synchronises two directory structures. */ -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); - } - - @Override - protected void error(Object obj, Throwable e) { - log.error(obj, e); - } - - @Override - protected boolean isDebugEnabled() { - return log.isDebugEnabled(); - } - - @Override - protected void debug(Object obj) { - log.debug(obj); - } -} 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.jcr/src/org/argeo/sync/SyncException.java b/org.argeo.jcr/src/org/argeo/sync/SyncException.java new file mode 100644 index 000000000..89bf869a2 --- /dev/null +++ b/org.argeo.jcr/src/org/argeo/sync/SyncException.java @@ -0,0 +1,18 @@ +package org.argeo.sync; + +/** Commons exception for sync */ +public class SyncException extends RuntimeException { + private static final long serialVersionUID = -3371314343580218538L; + + public SyncException(String message) { + super(message); + } + + public SyncException(String message, Throwable cause) { + super(message, cause); + } + + public SyncException(Object source, Object target, Throwable cause) { + super("Cannot sync from " + source + " to " + target, cause); + } +} 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(); + } + + } +}