package cloudsync.helper;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Properties;
import cloudsync.exceptions.InfoException;
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.apache.commons.cli.ParseException;
import org.apache.commons.lang3.StringUtils;
import cloudsync.exceptions.CloudsyncException;
import cloudsync.exceptions.UsageException;
import cloudsync.model.options.NetworkErrorType;
import cloudsync.model.options.FileErrorType;
import cloudsync.model.options.ExistingType;
import cloudsync.model.Item;
import cloudsync.model.options.FollowLinkType;
import cloudsync.model.options.PermissionType;
import cloudsync.model.options.SyncType;
public class CmdOptions
{
private final Options options;
private final List<Option> positions;
private final String[] args;
private String passphrase;
private Properties prop;
private SyncType type;
private String path;
private String name;
private Integer history;
private String[] includePatterns;
private String[] excludePatterns;
private String logfilePath;
private String cachefilePath;
private String lockfilePath;
private String pidfilePath;
private PermissionType permissions;
private boolean nocache;
private boolean forcestart;
private boolean dryrun;
private boolean showProgress;
private NetworkErrorType networkErrorBehavior;
private FileErrorType fileErrorBehavior;
private boolean noencryption;
private FollowLinkType followlinks;
private ExistingType existingBehavior;
private String remoteConnector;
private int retries;
private int waitretry;
private long minTmpFileSize;
public CmdOptions(final String[] args)
{
this.args = args;
positions = new ArrayList<>();
options = new Options();
Option option = Option.builder("b")
.hasArg()
.argName("path")
.desc("Create or refresh backup of <path>")
.longOpt(SyncType.BACKUP.getName())
.build();
options.addOption(option);
positions.add(option);
option = Option.builder("r")
.hasArg()
.argName("path")
.desc("Restore a backup into <path>")
.longOpt(SyncType.RESTORE.getName())
.build();
options.addOption(option);
positions.add(option);
option = Option.builder("c")
.hasArg()
.argName("path")
.desc("Repair 'cloudsync*.cache' file and put leftover file into <path>")
.longOpt(SyncType.CLEAN.getName())
.build();
options.addOption(option);
positions.add(option);
option = Option.builder("l")
.desc("List the contents of an backup")
.longOpt(SyncType.LIST.getName())
.build();
options.addOption(option);
positions.add(option);
option = Option.builder("n")
.hasArg()
.argName("name")
.desc("Backup name of --backup, --restore, --clean or --list")
.longOpt("name")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("path")
.desc("Config file path. Default is './config/cloudsync.config'")
.longOpt("config")
.build();
options.addOption(option);
positions.add(option);
String description = "How to handle symbolic links\n";
description += "<extern> - follow symbolic links if the target is outside from the current directory hierarchy - (default)\n";
description += "<all> - follow all symbolic links\n";
description += "<none> - don't follow any symbolic links";
option = Option.builder()
.hasArg()
.argName("extern|all|none")
.desc(description)
.longOpt("followlinks")
.build();
options.addOption(option);
positions.add(option);
description = "Behavior on files that exists localy during --restore\n";
description += "<stop> - stop immediately - (default)\n";
description += "<update> - replace file\n";
description += "<skip> - skip file\n";
description += "<rename> - extend the name with an autoincrement number";
option = Option.builder()
.hasArg()
.argName("stop|update|skip|rename")
.desc(description)
.longOpt("existing")
.build();
options.addOption(option);
positions.add(option);
description = "Before remove or update a file or folder move it to a history folder.\n";
description += "Use a maximum of <count> history folders";
option = Option.builder()
.hasArg()
.argName("count")
.desc(description)
.longOpt("history")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("pattern")
.desc("Include content of --backup, --restore and --list if the path matches the regex based ^<pattern>$. Multiple patterns can be separated with an '|' character.")
.longOpt("include")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("pattern")
.desc("Exclude content of --backup, --restore and --list if the path matches the regex based ^<pattern>$. Multiple patterns can be separated with an '|' character.")
.longOpt("exclude")
.build();
options.addOption(option);
positions.add(option);
description = "Behavior how to handle acl permissions during --restore\n";
description += "<set> - set all permissions and ownerships - (default)\n";
description += "<ignore> - ignores all permissions and ownerships\n";
description += "<try> - ignores invalid and not assignable permissions and ownerships\n";
option = Option.builder()
.hasArg()
.argName("set|ignore|try")
.desc(description)
.longOpt("permissions")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.desc("Don't use 'cloudsync*.cache' file for --backup or --list (much slower)")
.longOpt("nocache")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.desc("Ignore a existing pid file. Should only be used after a previous crashed job.")
.longOpt("forcestart")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.desc("Don't encrypt uploaded data")
.longOpt("noencryption")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.desc("Perform a trial run of --backup or --restore with no changes made.")
.longOpt("dry-run")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.desc("Show progress during transfer and encryption.")
.longOpt("progress")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("number")
.desc("Number of network operation retries before an error is thrown (default: 6).")
.longOpt("retries")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("seconds")
.desc("Number of seconds between 2 retries (default: 10).")
.longOpt("waitretry")
.build();
options.addOption(option);
positions.add(option);
description = "How to continue on network problems\n";
description += "<exception> - Throw an exception - (default)\n";
description += "<ask> - Show a command prompt (Y/n) to continue\n";
description += "<continue> - Show a warning and continue\n";
option = Option.builder()
.hasArg()
.argName("exception|ask|continue")
.desc(description)
.longOpt("network-error")
.build();
options.addOption(option);
positions.add(option);
description = "How to continue on blocked files or permission problems\n";
description += "<exception> - Throw an exception - (default)\n";
description += "<message> - Show a error log message\n";
option = Option.builder()
.hasArg()
.argName("exception|message")
.desc(description)
.longOpt("file-error")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("path")
.desc("Log message to <path>")
.longOpt("logfile")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("path")
.desc("Cache data to <path>")
.longOpt("cachefile")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder()
.hasArg()
.argName("size")
.desc("Minimum file size <size> in bytes to use tmp files (default: 134217728)")
.longOpt("min_tmp_file_size")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder("v")
.desc("Show version number")
.longOpt("version")
.build();
options.addOption(option);
positions.add(option);
option = Option.builder("h")
.desc("Show this help")
.longOpt("help")
.build();
options.addOption(option);
positions.add(option);
}
public void parse() throws UsageException, CloudsyncException, InfoException
{
final CommandLineParser parser = new DefaultParser();
CommandLine cmd;
try
{
cmd = parser.parse(options, args);
}
catch (ParseException e)
{
throw new UsageException(e.getMessage());
}
type = null;
path = null;
if ((path = cmd.getOptionValue(SyncType.BACKUP.getName())) != null)
{
type = SyncType.BACKUP;
}
else if ((path = cmd.getOptionValue(SyncType.RESTORE.getName())) != null)
{
type = SyncType.RESTORE;
}
else if ((path = cmd.getOptionValue(SyncType.CLEAN.getName())) != null)
{
type = SyncType.CLEAN;
}
else if (cmd.hasOption(SyncType.LIST.getName()))
{
type = SyncType.LIST;
}
String config = cmd.getOptionValue("config", "." + Item.SEPARATOR + "config" + Item.SEPARATOR + "cloudsync.config");
if (config.startsWith("." + Item.SEPARATOR))
{
config = System.getProperty("user.dir") + Item.SEPARATOR + config;
}
boolean configValid = new File(config).isFile();
prop = new Properties();
try
{
prop.load(new FileInputStream(config));
}
catch (final IOException e)
{
configValid = false;
}
name = getOptionValue(cmd, "name", null);
remoteConnector = prop.getProperty("REMOTE_CONNECTOR");
String value = getOptionValue(cmd, "followlinks", FollowLinkType.EXTERNAL.getName());
followlinks = FollowLinkType.fromStringIgnoreCase(value);
value = getOptionValue(cmd, "existing", SyncType.CLEAN.equals(type) ? ExistingType.RENAME.getName() : ExistingType.STOP.getName());
existingBehavior = ExistingType.fromStringIgnoreCase(value);
value = getOptionValue(cmd, "permissions", PermissionType.SET.getName());
permissions = PermissionType.fromStringIgnoreCase(value);
history = SyncType.BACKUP.equals(type) ? Integer.parseInt(getOptionValue(cmd, "history", "0")) : 0;
try
{
retries = Integer.parseInt(getOptionValue(cmd, "retries", "6"));
}
catch (NumberFormatException e)
{
retries = 0;
}
try
{
waitretry = Integer.parseInt(getOptionValue(cmd, "waitretry", "10"));
}
catch (NumberFormatException e)
{
waitretry = 0;
}
try
{
minTmpFileSize = Long.parseLong( getOptionValue(cmd, "min_tmp_file_size", "134217728" ) );
}
catch (NumberFormatException e)
{
// 128MB
minTmpFileSize = 134217728;
}
value = getOptionValue(cmd, "network-error", "exception");
networkErrorBehavior = NetworkErrorType.fromStringIgnoreCase( value);
value = getOptionValue(cmd, "file-error", "exception");
fileErrorBehavior = FileErrorType.fromStringIgnoreCase( value);
nocache = cmd.hasOption("nocache") || SyncType.CLEAN.equals(type);
forcestart = cmd.hasOption("forcestart");
dryrun = cmd.hasOption("dry-run");
showProgress = cmd.hasOption("progress");
noencryption = cmd.hasOption("noencryption");
String pattern = getOptionValue(cmd, "include", null);
if (pattern != null) includePatterns = pattern.contains("|") ? pattern.split("\\|") : new String[] { pattern };
pattern = getOptionValue(cmd, "exclude", null);
if (pattern != null) excludePatterns = pattern.contains("|") ? pattern.split("\\|") : new String[] { pattern };
if (!StringUtils.isEmpty(name))
{
logfilePath = Helper.preparePath(getOptionValue(cmd, "logfile", null), name);
cachefilePath = Helper.preparePath(getOptionValue(cmd, "cachefile", null), name);
if( !StringUtils.isEmpty(cachefilePath))
{
pidfilePath = cachefilePath.substring(0, cachefilePath.lastIndexOf(".")) + ".pid";
lockfilePath = cachefilePath.substring(0, cachefilePath.lastIndexOf(".")) + ".lock";
}
}
final boolean baseValid = SyncType.LIST.equals(type) || (path != null && new File(path).isDirectory());
boolean logfileValid = logfilePath == null || new File(logfilePath).getParentFile().isDirectory();
boolean cachefileValid = cachefilePath == null || new File(cachefilePath).getParentFile().isDirectory();
if( cmd.hasOption("version") )
{
throw new InfoException("cloudsync " + getClass().getPackage().getImplementationVersion());
}
else if (cmd.hasOption("help") || type == null || name == null || followlinks == null || existingBehavior == null || retries == 0 || waitretry == 0
|| permissions == null || !baseValid || !configValid || !logfileValid || !cachefileValid)
{
int possibleWrongOptions = cmd.getOptions().length;
if (cmd.hasOption("help")) possibleWrongOptions--;
List<String> messages = new ArrayList<>();
if (possibleWrongOptions > 0)
{
messages.add("missing or wrong options\nerror(s):");
if (type == null)
{
messages.add(" You must specifiy --backup, --restore, --list or --clean");
}
else if (!baseValid)
{
messages.add(" --" + type.getName() + " <path> not valid");
}
if (name == null)
{
messages.add(" Missing --name <name>");
}
if (followlinks == null)
{
messages.add(" Wrong --followlinks <behavior> set");
}
if (existingBehavior == null)
{
messages.add(" Wrong --existing <behavior> set");
}
if (retries == 0)
{
messages.add(" Wrong --retries <number> set");
}
if (waitretry == 0)
{
messages.add(" Wrong --waitretry <seconds> set");
}
if (permissions == null)
{
messages.add(" Wrong --permissions <behavior> set");
}
if (!configValid)
{
messages.add(" --config <path> not valid");
}
if (!logfileValid)
{
messages.add(" --logfile <path> not valid");
}
if (!cachefileValid)
{
messages.add(" --cachefile <path> not valid");
}
}
else if (!cmd.hasOption("help") && !configValid)
{
messages.add(" No config file found");
}
throw new UsageException(StringUtils.join(messages, '\n'));
}
passphrase = prop.getProperty("PASSPHRASE");
if (StringUtils.isEmpty(passphrase))
{
throw new CloudsyncException("'PASSPHRASE' is not configured");
}
}
private String getOptionValue(CommandLine cmd, String key, String defaultValue)
{
String value = cmd.getOptionValue(key);
if (!StringUtils.isEmpty(value)) return value;
value = prop.getProperty(key.toUpperCase());
if (!StringUtils.isEmpty(value)) return value;
return defaultValue;
}
public void printHelp()
{
final HelpFormatter formatter = new HelpFormatter();
formatter.setWidth(120);
formatter.setOptionComparator(new Comparator<Option>()
{
@Override
public int compare(Option o1, Option o2)
{
if (positions.indexOf(o1) < positions.indexOf(o2)) return -1;
if (positions.indexOf(o1) > positions.indexOf(o2)) return 1;
return 0;
}
});
// formatter.setOptPrefix("");
formatter.printHelp("cloudsync <options>", options);
}
public String getPath()
{
return path;
}
public SyncType getType()
{
return type;
}
public String getName()
{
return name;
}
public String[] getIncludePatterns()
{
return includePatterns;
}
public String[] getExcludePatterns()
{
return excludePatterns;
}
public Integer getHistory()
{
return history;
}
public String getLogfilePath()
{
return logfilePath;
}
public PermissionType getPermissionType()
{
return permissions;
}
public boolean getNoCache()
{
return nocache;
}
public boolean getNoEncryption()
{
return noencryption;
}
public boolean getForceStart()
{
return forcestart;
}
public boolean isDryRun()
{
return dryrun;
}
public boolean showProgress()
{
return showProgress;
}
public NetworkErrorType getNetworkErrorBehavior()
{
return networkErrorBehavior;
}
public FileErrorType getFileErrorBehavior()
{
return fileErrorBehavior;
}
public int getRetries()
{
return retries;
}
public int getWaitRetry()
{
return waitretry;
}
public long getMinTmpFileSise()
{
return minTmpFileSize;
}
public FollowLinkType getFollowLinks()
{
return followlinks;
}
public ExistingType getExistingBehavior()
{
return existingBehavior;
}
public String getProperty(String key)
{
return prop.getProperty(key);
}
public String getCacheFile()
{
return cachefilePath;
}
public String getLockFile()
{
return lockfilePath;
}
public String getPIDFile()
{
return pidfilePath;
}
public String getPassphrase()
{
return passphrase;
}
public String getRemoteConnector()
{
return remoteConnector;
}
public Charset getCharset(){ return Charset.defaultCharset(); }
}