/*
* This file is part of Fim - File Integrity Manager
*
* Copyright (C) 2017 Etienne Vrignaud
*
* Fim is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Fim is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Fim. If not, see <http://www.gnu.org/licenses/>.
*/
package org.fim;
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.Option.Builder;
import org.apache.commons.cli.Options;
import org.fim.command.AbstractCommand;
import org.fim.command.CommitCommand;
import org.fim.command.DetectCorruptionCommand;
import org.fim.command.DiffCommand;
import org.fim.command.DisplayIgnoredFilesCommand;
import org.fim.command.FindDuplicatesCommand;
import org.fim.command.HelpCommand;
import org.fim.command.InitCommand;
import org.fim.command.LogCommand;
import org.fim.command.PurgeStatesCommand;
import org.fim.command.RemoveDuplicatesCommand;
import org.fim.command.ResetFileAttributesCommand;
import org.fim.command.RollbackCommand;
import org.fim.command.StatusCommand;
import org.fim.command.VersionCommand;
import org.fim.command.exception.BadFimUsageException;
import org.fim.command.exception.DontWantToContinueException;
import org.fim.command.exception.RepositoryException;
import org.fim.internal.SettingsManager;
import org.fim.model.Command;
import org.fim.model.Command.FimReposConstraint;
import org.fim.model.Context;
import org.fim.model.Ignored;
import org.fim.util.Logger;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Scanner;
import static org.fim.model.HashMode.dontHash;
import static org.fim.model.HashMode.hashAll;
import static org.fim.model.HashMode.hashMediumBlock;
import static org.fim.model.HashMode.hashSmallBlock;
public class Fim {
private List<AbstractCommand> commands = buildCommands();
private Options options = buildOptions();
public static void main(String[] args) throws Exception {
try {
Fim fim = new Fim();
Context context = new Context();
fim.run(args, context);
} catch (DontWantToContinueException ex) {
System.exit(0);
} catch (BadFimUsageException ex) {
System.exit(-1);
} catch (RepositoryException ex) {
System.exit(-2);
}
System.exit(0);
}
private List<AbstractCommand> buildCommands() {
return Arrays.asList(
new InitCommand(),
new CommitCommand(),
new StatusCommand(),
new DiffCommand(),
new ResetFileAttributesCommand(),
new DetectCorruptionCommand(),
new FindDuplicatesCommand(),
new RemoveDuplicatesCommand(),
new LogCommand(),
new DisplayIgnoredFilesCommand(),
new RollbackCommand(),
new PurgeStatesCommand(),
new HelpCommand(this),
new VersionCommand());
}
private Options buildOptions() {
Options opts = new Options();
opts.addOption(buildOption("d", "directory", "Run Fim into the specified directory").hasArg().build());
opts.addOption(buildOption("e", "errors", "Display execution error details").build());
opts.addOption(buildOption("M", "master-fim-repository", "Fim repository directory that you want to use as remote master.\n" +
"Only for the 'remove-duplicates' command").hasArg().build());
opts.addOption(buildOption("n", "do-not-hash", "Do not hash file content. Uses only file names and modification dates").build());
opts.addOption(buildOption("s", "super-fast-mode", "Use super-fast mode. Hash only 3 small blocks.\n" +
"One at the beginning, one in the middle and one at the end").build());
opts.addOption(buildOption("f", "fast-mode", "Use fast mode. Hash only 3 medium blocks.\n" +
"One at the beginning, one in the middle and one at the end").build());
opts.addOption(buildOption("h", "help", "Prints the Fim help").build());
opts.addOption(buildOption("i", "ignore", "Ignore some difference during State comparison. You can ignore:\n" +
"- attrs: File attributes\n" +
"- dates: Modification and creation dates\n" +
"- renamed: Renamed files\n" +
"- all: All of the above\n" +
"You can specify multiple kind of difference to ignore separated by a comma.\n" +
"For example: -i attrs,dates,renamed").hasArg().valueSeparator(',').build());
opts.addOption(buildOption("l", "use-last-state", "Use the last committed State.\n" +
"Both for the 'find-duplicates' and 'remove-duplicates' commands").build());
opts.addOption(buildOption("m", "comment", "Comment to set during init and commit").hasArg().build());
opts.addOption(buildOption("c", "", "Deprecated option used to set the init or commit comment. Use '-m' instead").hasArg().build());
opts.addOption(buildOption("o", "output-max-lines", "Change the maximum number lines displayed for the same kind of modification.\n" +
"Default value is 200 lines").hasArg().build());
opts.addOption(buildOption("p", "purge-states", "Purge previous States if the commit succeed").build());
opts.addOption(buildOption("q", "quiet", "Do not display details").build());
opts.addOption(buildOption("t", "thread-count", "Number of thread used to hash file contents in parallel.\n" +
"By default, this number is dynamic and depends on the disk throughput").hasArg().build());
opts.addOption(buildOption("v", "version", "Prints the Fim version").build());
opts.addOption(buildOption("y", "always-yes", "Always yes to every questions").build());
return opts;
}
protected void run(String[] args, Context context) throws Exception {
String[] filteredArgs = filterEmptyArgs(args);
if (filteredArgs.length < 1) {
youMustSpecifyACommandToRun(null);
}
Command command = null;
String[] optionArgs = filteredArgs;
String firstArg = filteredArgs[0];
if (firstArg.startsWith("-")) {
firstArg = null;
} else {
optionArgs = Arrays.copyOfRange(filteredArgs, 1, filteredArgs.length);
command = findCommand(firstArg);
}
CommandLineParser cmdLineGnuParser = new DefaultParser();
try {
CommandLine commandLine = cmdLineGnuParser.parse(options, optionArgs);
String ignoredKinds = commandLine.getOptionValue('i');
if (ignoredKinds != null) {
parseIgnored(context, ignoredKinds);
}
if (commandLine.hasOption('c')) {
Logger.out.println("The '-c' option is deprecated and will be removed in the future. use '-m' instead\n");
context.setComment(commandLine.getOptionValue('c', context.getComment()));
}
context.setVerbose(!commandLine.hasOption('q'));
context.setComment(commandLine.getOptionValue('m', context.getComment()));
context.setUseLastState(commandLine.hasOption('l'));
context.setPurgeStates(commandLine.hasOption('p'));
context.setAlwaysYes(commandLine.hasOption('y'));
context.setDisplayStackTrace(commandLine.hasOption('e'));
if (commandLine.hasOption('M')) {
String masterFimRepositoryDir = commandLine.getOptionValue('M');
if (!Files.exists(Paths.get(masterFimRepositoryDir))) {
Logger.error(String.format("Master Fim repository directory '%s' does not exist", masterFimRepositoryDir));
throw new BadFimUsageException();
}
context.setMasterFimRepositoryDir(masterFimRepositoryDir);
}
if (commandLine.hasOption('d')) {
context.setCurrentDirectory(Paths.get(commandLine.getOptionValue('d')));
}
if (commandLine.hasOption('t')) {
context.setThreadCount(Integer.parseInt(commandLine.getOptionValue('t', "-1")));
context.setThreadCountSpecified(true);
}
context.setDynamicScaling(context.getThreadCount() <= 0);
context.setTruncateOutput(Integer.parseInt(commandLine.getOptionValue('o', "200")));
if (context.getTruncateOutput() < 0) {
context.setTruncateOutput(0);
}
if (commandLine.hasOption('n')) {
context.setHashMode(dontHash);
} else if (commandLine.hasOption('s')) {
context.setHashMode(hashSmallBlock);
} else if (commandLine.hasOption('f')) {
context.setHashMode(hashMediumBlock);
} else {
context.setHashMode(hashAll);
}
if (commandLine.hasOption('h')) {
command = new HelpCommand(this);
} else if (commandLine.hasOption('v')) {
command = new VersionCommand();
}
} catch (Exception ex) {
Logger.error("Exception parsing command line", ex, context.isDisplayStackTrace());
throw new BadFimUsageException();
}
if (command == null) {
youMustSpecifyACommandToRun(firstArg);
}
FimReposConstraint constraint = command.getFimReposConstraint();
if (constraint == FimReposConstraint.MUST_NOT_EXIST) {
setRepositoryRootDir(context, context.getAbsoluteCurrentDirectory(), false);
if (Files.exists(context.getRepositoryDotFimDir()) || Files.exists(context.getRepositoryStatesDir())) {
Logger.error("Fim repository already exist");
throw new BadFimUsageException();
}
if (!Files.isWritable(context.getRepositoryRootDir())) {
Logger.error(String.format("Not allowed to create the '%s' directory that holds the Fim repository", context.getRepositoryDotFimDir()));
throw new RepositoryException();
}
} else if (constraint == FimReposConstraint.MUST_EXIST) {
findRepositoryRootDir(context);
if (!Files.exists(context.getRepositoryStatesDir())) {
Logger.error("Fim repository does not exist. Please run 'fim init' before.");
throw new BadFimUsageException();
}
if (!Files.isWritable(context.getRepositoryStatesDir())) {
Logger.error(String.format("Not allowed to modify States into the '%s' directory", context.getRepositoryStatesDir()));
throw new RepositoryException();
}
SettingsManager settingsManager = new SettingsManager(context);
if (!Files.isWritable(settingsManager.getSettingsFile())) {
Logger.error(String.format("Not allowed to save settings into the '%s' directory", context.getRepositoryDotFimDir()));
throw new RepositoryException();
}
}
command.execute(context.clone());
}
private void parseIgnored(Context context, String ignoredKinds) {
Ignored ignored = context.getIgnored();
try (Scanner scanner = new Scanner(ignoredKinds)) {
scanner.useDelimiter(",");
while (scanner.hasNext()) {
String token = scanner.next();
if (token.length() == 0) {
continue;
}
if ("attrs".equals(token)) {
ignored.setAttributesIgnored(true);
} else if ("dates".equals(token)) {
ignored.setDatesIgnored(true);
} else if ("renamed".equals(token)) {
ignored.setRenamedIgnored(true);
} else if ("all".equals(token)) {
ignored.setAttributesIgnored(true);
ignored.setDatesIgnored(true);
ignored.setRenamedIgnored(true);
} else {
Logger.error(String.format("'%s' unknown as difference kind to ignore.", token));
throw new BadFimUsageException();
}
}
}
}
private void findRepositoryRootDir(Context context) {
boolean invokedFromSubDirectory = false;
Path directory = context.getAbsoluteCurrentDirectory();
while (directory != null) {
Path dotFimDir = directory.resolve(Context.DOT_FIM_DIR);
if (Files.exists(dotFimDir)) {
setRepositoryRootDir(context, directory, invokedFromSubDirectory);
return;
}
directory = directory.getParent();
invokedFromSubDirectory = true;
}
}
private void setRepositoryRootDir(Context context, Path directory, boolean invokedFromSubDirectory) {
context.setRepositoryRootDir(directory);
context.setInvokedFromSubDirectory(invokedFromSubDirectory);
}
private Command findCommand(final String cmdName) {
for (final Command command : commands) {
if (command.getCmdName().equals(cmdName)) {
return command;
}
if (command.getShortCmdName().length() > 0 && command.getShortCmdName().equals(cmdName)) {
return command;
}
}
return null;
}
private Builder buildOption(String opt, String longOpt, String description) {
Builder builder = Option.builder(opt);
builder.longOpt(longOpt);
builder.desc(description);
return builder;
}
protected String[] filterEmptyArgs(String[] args) {
List<String> filteredArgs = new ArrayList<>();
for (String arg : args) {
if (arg.length() > 0) {
filteredArgs.add(arg);
}
}
return filteredArgs.toArray(new String[0]);
}
private void youMustSpecifyACommandToRun(String firstArg) {
if (firstArg != null) {
Logger.error(String.format("'%s' is not a fim command. See 'fim --help'.", firstArg));
} else {
Logger.error("You must specify the command to run. See 'fim --help'");
}
throw new BadFimUsageException();
}
public void printUsage() {
StringBuilder usage = new StringBuilder();
usage.append("\n");
usage.append("File Integrity Checker\n");
usage.append("\n");
usage.append("Available commands:\n");
for (final Command command : commands) {
String cmdName;
if (command.getShortCmdName() != null && command.getShortCmdName().length() > 0) {
cmdName = command.getShortCmdName() + " / " + command.getCmdName();
} else {
cmdName = command.getCmdName();
}
usage.append(String.format(" %-26s %s%n", cmdName, command.getDescription()));
}
usage.append("\n");
usage.append("Available options:\n");
PrintWriter writer = new PrintWriter(Logger.out);
HelpFormatter helpFormatter = new HelpFormatter();
Logger.newLine();
helpFormatter.printHelp(writer, 120, "fim <command>", usage.toString(), options, 5, 3, "", true);
writer.flush();
Logger.newLine();
}
}