package jdrivesync.cli;
import jdrivesync.constants.Constants;
import jdrivesync.exception.JDriveSyncException;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Optional;
public class CliParser {
private enum Argument {
Help("-h", "--help", "Prints this help."),
LocalRootDir("-l", "--local-dir", "Provides the local directory that should be synchronized.", "<local-dir>"),
RemoteRootDir("-r", "--remote-dir", "Provides the remote directory that should be synchronized.", "<remote-dir>"),
AuthenticationFile("-a", "--authentication-file", "Use given authentication file instead of default one (~/.jdrivesync).", "<auth-file>"),
DryRun(null, "--dry-run", "Simulates all data manipulating operations (dry run)."),
Delete(null, "--delete", "Deletes all files instead of moving them to trash."),
Checksum("-c", "--checksum", "Use MD5 checksum instead of last modification timestamp of file."),
IgnoreFile("-i", "--ignore-file", "Provides a file with newline separated file and/or path name patterns that should be ignored.", "<ignore-file>"),
SyncUp("-u", "--up", "Synchronization is performed from the local to the remote site (default)."),
SyncDown("-d", "--down", "Synchronization is performed from the remote to the local site."),
HtmlReport(null, "--html-report", "Creates an HTML report of the synchronization."),
MaxFileSize("-m", "--max-file-size", "Provides the maximum file size in MB.", "<maxFileSize>"),
HttpChunkSize(null, "--http-chunk-size", "The size of a chunk in MB used for chunked uploads (default: 10MB)."),
NetworkNumberOfReries(null, "--network-number-of-retries", "The number of times how often a request is retried (default: 3)."),
NetworkSleepBetweenRetries(null, "--network-sleep-between-retries", "The number of seconds to sleep between retries (default: 10)."),
Verbose("-v", "--verbose", "Verbose output"),
LogFile(null, "--log-file", "The location for the log file.", "<log-file>"),
NoDelete(null, "--no-delete", "Do not delete files.");
//Password("-p", "--password", "The password used to encrypt/decrypt the files.", "<password>"),
//EncryptFile("-e", "--encrypt-files", "Provides a file with newline separated file and/or path name patterns that should be encrypted.", "<encrypt-file>");
private final String shortOption;
private final String longOption;
private final String description;
private final Optional<String> argument;
Argument(String shortOption, String longOption, String description) {
this.shortOption = shortOption;
this.longOption = longOption;
this.description = description;
this.argument = Optional.empty();
}
Argument(String shortOption, String longOption, String description, String argument) {
this.shortOption = shortOption;
this.longOption = longOption;
this.description = description;
this.argument = Optional.of(argument);
}
public boolean matches(String arg) {
boolean matches = false;
if (shortOption != null && shortOption.equals(arg)) {
matches = true;
}
if (longOption != null && longOption.equals(arg)) {
matches = true;
}
return matches;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (shortOption != null) {
sb.append(shortOption);
}
if (longOption != null) {
if (shortOption != null) {
sb.append(",");
}
sb.append(longOption);
}
if (argument.isPresent()) {
sb.append(" ");
sb.append(argument.get());
}
sb.append("\n");
sb.append("\t");
sb.append(description);
return sb.toString();
}
}
public Options parse(String[] args) throws IllegalArgumentException {
Options options = new Options();
StringArrayEnumeration sae = new StringArrayEnumeration(args);
while (sae.hasMoreElements()) {
String arg = sae.nextElement();
Argument argument = toArgument(arg);
if (argument == Argument.Help) {
printHelp();
} else if (argument == Argument.LocalRootDir) {
String localRootDir = getOptionWithArgument(arg, sae);
File file = validateLocalRootDirArg(localRootDir);
options.setLocalRootDir(Optional.of(file));
} else if (argument == Argument.RemoteRootDir) {
String remoteRootDir = getOptionWithArgument(arg, sae);
options.setRemoteRootDir(Optional.of(remoteRootDir));
} else if (argument == Argument.AuthenticationFile) {
String authenticationFile = getOptionWithArgument(arg, sae);
options.setAuthenticationFile(Optional.of(authenticationFile));
} else if (argument == Argument.DryRun) {
options.setDryRun(true);
} else if (argument == Argument.Delete) {
options.setDeleteFiles(true);
} else if (argument == Argument.Checksum) {
options.setUseChecksum(true);
} else if (argument == Argument.IgnoreFile) {
String patternArg = getOptionWithArgument(arg, sae);
List<String> lines = readFile(patternArg);
FileNamePatterns ignoreFiles = FileNamePatterns.create(lines);
options.setIgnoreFiles(ignoreFiles);
} else if (argument == Argument.HtmlReport) {
options.setHtmlReport(true);
} else if (argument == Argument.SyncUp) {
options.setSyncDirection(SyncDirection.Up);
} else if (argument == Argument.SyncDown) {
options.setSyncDirection(SyncDirection.Down);
} else if (argument == Argument.MaxFileSize) {
String option = getOptionWithArgument(arg, sae);
Long maxFileSizeInteger;
try {
maxFileSizeInteger = Long.valueOf(option);
} catch (NumberFormatException e) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is not an integer.");
}
options.setMaxFileSize(Optional.of(maxFileSizeInteger * Constants.MB));
} else if (argument == Argument.HttpChunkSize) {
String option = getOptionWithArgument(arg, sae);
long httpChunkSizeMB;
try {
httpChunkSizeMB = Long.valueOf(option);
} catch (NumberFormatException e) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is not an integer.");
}
long httpChunkSizeBytes = httpChunkSizeMB * Constants.MB;
httpChunkSizeBytes = (httpChunkSizeBytes / 256) * 256; // chunk size must be multiple of 256
if (httpChunkSizeMB <= 0) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is a negative integer or zero.");
}
options.setHttpChunkSizeInBytes(httpChunkSizeBytes);
} else if (argument == Argument.NetworkNumberOfReries) {
String option = getOptionWithArgument(arg, sae);
int networkNumberOfRetries;
try {
networkNumberOfRetries = Integer.valueOf(option);
} catch (NumberFormatException e) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is not an integer.");
}
if (networkNumberOfRetries < 0) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is a negative integer.");
}
options.setNetworkNumberOfAttempts(networkNumberOfRetries);
} else if (argument == Argument.NetworkSleepBetweenRetries) {
String option = getOptionWithArgument(arg, sae);
int optionAsInteger;
try {
optionAsInteger = Integer.valueOf(option);
} catch (NumberFormatException e) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is not an integer.");
}
if (optionAsInteger <= 0) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is a negative integer or zero.");
}
options.setNetworkSleepBetweenAttempts(optionAsInteger * 1000);
} else if (argument == Argument.Verbose) {
options.setVerbose(true);
} else if (argument == Argument.LogFile) {
String option = getOptionWithArgument(arg, sae);
Path path = Paths.get(option);
if (Files.isDirectory(path)) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Argument for option '" + arg + "' is a directory and not a file.");
}
if (!Files.exists(path)) {
try {
path = Files.createFile(path);
} catch (IOException e) {
throw new JDriveSyncException(JDriveSyncException.Reason.IOException, String.format("Failed to create log file '%s': %s", path.toString(), e.getClass().getSimpleName() + ": " + e.getMessage()), e);
}
}
if (!Files.isWritable(path)) {
throw new JDriveSyncException(JDriveSyncException.Reason.IOException, String.format("The log file '%s' is not writable.", path.toString()));
}
options.setLogFile(Optional.of(path));
} else if (argument == Argument.NoDelete) {
options.setNoDelete(true);
} else {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "The parameter '" + arg + "' is not valid.");
}
}
checkForMandatoryOptions(options);
normalizeRemoteRootDir(options);
return options;
}
private Argument toArgument(String arg) {
for (Argument currentArgument : Argument.values()) {
if (currentArgument.matches(arg)) {
return currentArgument;
}
}
return null;
}
private File validateLocalRootDirArg(String localRootDir) {
File file = new File(localRootDir);
if (!file.exists()) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, String.format("'%s' does not exist.", localRootDir));
}
if (!file.canRead()) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, String.format("Directory '%s' is not readable.", localRootDir));
}
if (!file.isDirectory()) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, String.format("'%s' is not a directory.", localRootDir));
}
return file;
}
public void normalizeRemoteRootDir(Options options) {
if (options.getRemoteRootDir().isPresent()) {
String remoteRootDir = options.getRemoteRootDir().get();
remoteRootDir = remoteRootDir.trim();
remoteRootDir = remoteRootDir.replace("\\", "/");
if (remoteRootDir.startsWith("/")) {
remoteRootDir = remoteRootDir.substring(1, remoteRootDir.length());
}
options.setRemoteRootDir(Optional.of(remoteRootDir));
}
}
public static void printHelp() {
System.out.println("Available parameters:");
for (Argument currentArg : Argument.values()) {
System.out.println(currentArg.toString());
}
throw new JDriveSyncException(JDriveSyncException.Reason.NormalTermination);
}
private void checkForMandatoryOptions(Options options) {
boolean valid = true;
String message = null;
if (!options.getLocalRootDir().isPresent()) {
message = "Please specify a local directory that should be synchronized.";
valid = false;
}
if (!valid) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, message);
}
}
private String getOptionWithArgument(String option, StringArrayEnumeration sae) {
if (sae.hasMoreElements()) {
String value = sae.nextElement();
if (toArgument(value) != null) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, String.format("Missing argument for option %s.", option));
}
return value;
} else {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, String.format("Missing argument for option %s.", option));
}
}
private List<String> readFile(String filename) {
Path path;
try {
path = Paths.get(filename);
} catch (Exception e) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "'" + filename + "' does not denote a valid path: " + e.getMessage(), e);
}
if (!Files.exists(path)) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "'" + filename + "' does not exist.");
}
try {
return Files.readAllLines(path, Charset.defaultCharset());
} catch (IOException e) {
throw new JDriveSyncException(JDriveSyncException.Reason.InvalidCliParameter, "Could not read file '" + path + "':" + e.getMessage(), e);
}
}
}