commit 3351daaef4f50f27fefde6ba2bc8308796bfb5f9 Author: Tilman Date: Wed Oct 26 09:56:33 2011 +0200 Inital commit diff --git a/.classpath b/.classpath new file mode 100644 index 0000000..24785a7 --- /dev/null +++ b/.classpath @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/.project b/.project new file mode 100644 index 0000000..a4d20f0 --- /dev/null +++ b/.project @@ -0,0 +1,23 @@ + + + SyncTool + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.maven.ide.eclipse.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.maven.ide.eclipse.maven2Nature + + diff --git a/.settings/org.eclipse.core.resources.prefs b/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 0000000..80b8578 --- /dev/null +++ b/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,3 @@ +#Thu Mar 17 11:56:15 CET 2011 +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/.settings/org.eclipse.core.runtime.prefs b/.settings/org.eclipse.core.runtime.prefs new file mode 100644 index 0000000..edb245c --- /dev/null +++ b/.settings/org.eclipse.core.runtime.prefs @@ -0,0 +1,3 @@ +#Thu Mar 17 11:56:15 CET 2011 +eclipse.preferences.version=1 +line.separator=\n diff --git a/.settings/org.eclipse.jdt.core.prefs b/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..3d385dc --- /dev/null +++ b/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,13 @@ +#Sat Feb 19 22:13:15 CET 2011 +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.assertIdentifier=error +org.eclipse.jdt.core.compiler.problem.enumIdentifier=error +org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning +org.eclipse.jdt.core.compiler.source=1.6 diff --git a/.settings/org.maven.ide.eclipse.prefs b/.settings/org.maven.ide.eclipse.prefs new file mode 100644 index 0000000..6b837fc --- /dev/null +++ b/.settings/org.maven.ide.eclipse.prefs @@ -0,0 +1,9 @@ +#Fri Feb 18 19:10:50 CET 2011 +activeProfiles= +eclipse.preferences.version=1 +fullBuildGoals=process-test-resources +includeModules=false +resolveWorkspaceProjects=true +resourceFilterGoals=process-resources resources\:testResources +skipCompilerPlugin=true +version=1 diff --git a/notes.txt b/notes.txt new file mode 100644 index 0000000..691b366 --- /dev/null +++ b/notes.txt @@ -0,0 +1,47 @@ +Weitere Optionen +- no sync for conflicting files +- exclude files (via regex) + + +Alternativer Ansatz: +Statt die Verzeichnisinhalte in Listen zu verwalten, könnte man die Liste beim Betreten eines Verzeichnisses +neu einlesen und sortieren. Wenn man "von unten" aus der Rekursion zurück kommt, macht man bei der Datei weiter, +die in der aktuellen Liste als nächstes kommt. +Vorteil: Geringerer Speicherbedarf, da keine Datei-Listen des gesamten Verzeichnis-Baumes oberhalb der aktuellen +Position gehalten werden müssen. +Nachteil: Ständiges Lesen und Sortieren der Verzeichnisinhalte. + + +Parameter: + -no-delete + -no-replace + -starttime + -logfile + -db -- path to database + -dry-run -- perform a trial run with no changes made + -hash -- compare the files by their MD5 hashes + -unidirectional/-oneway + -granularity + + -times -- synchronize modification times (only) + -owner/-group -- synchronize owner/group + -version + -exclude/-include + -silent/-verbose + -nowriteprotection + -help + -mailto -- http://openbook.galileodesign.de/javainsel5/javainsel16_010.htm#Rxx747java160100400063D1F048118 + -jabber -- http://www.igniterealtime.org/projects/smack/index.jsp + +Apache Commons: + DirectoryWalker: evtl. für Abbruch + FileUtils: copy & delete + +Tests: Dateien mit Mockito simulieren? + +Dateien kopieren: http://openbook.galileocomputing.de/javainsel8/javainsel_14_003.htm#mjdbd1ea4c51ff569d905c698602549180 +MD5 on the fly: http://stackoverflow.com/questions/304268/using-java-to-get-a-files-md5-checksum +MD5 direkt: String md5 = org.apache.commons.codec.digest.DigestUtils.md5Hex(new FileInputStream(srcFiles[0])); + +twalther42@jabber.org +dt1Ge3kEf diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..0314205 --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ + + 4.0.0 + + SyncTool + synctool + 1.2.1 + jar + + SyncTool + Directory Synchronization Tool + http://www.tilman.de/programme/synctool + + + UTF-8 + + + + + junit + junit + 3.8.1 + test + + + com.martiansoftware + jsap + 2.1 + jar + compile + + + log4j + log4j + 1.2.14 + jar + compile + + + com.h2database + h2 + 1.3.150 + jar + compile + + + commons-codec + commons-codec + 1.4 + jar + compile + + + commons-io + commons-io + 2.0.1 + jar + compile + + + jivesoftware + smack + 3.1.0 + jar + compile + + + jivesoftware + smackx + 3.1.0 + jar + compile + + + org.hsqldb + hsqldb + 2.0.0 + jar + compile + + + diff --git a/src/main/java/de/tilman/log4j/JabberAppender.java b/src/main/java/de/tilman/log4j/JabberAppender.java new file mode 100644 index 0000000..30063cb --- /dev/null +++ b/src/main/java/de/tilman/log4j/JabberAppender.java @@ -0,0 +1,90 @@ +/* + * Copyright 2011 Tilman Walther + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package de.tilman.log4j; + +import org.apache.log4j.AppenderSkeleton; +import org.apache.log4j.Logger; +import org.apache.log4j.PatternLayout; +import org.apache.log4j.spi.LoggingEvent; +import org.jivesoftware.smack.Chat; +import org.jivesoftware.smack.ConnectionConfiguration; +import org.jivesoftware.smack.MessageListener; +import org.jivesoftware.smack.XMPPConnection; +import org.jivesoftware.smack.XMPPException; +import org.jivesoftware.smack.packet.Message; + + + +/** + * A simple log4j appender that connects the logger to an XMPP recipient. + * + * @author Tilman Walther + */ +public class JabberAppender extends AppenderSkeleton { + + private final static Logger log = Logger.getLogger(JabberAppender.class); + + XMPPConnection connection; + String recipient; + Chat chat; + + public JabberAppender(String recipient, String server, String user, String password) throws XMPPException { + this.setLayout(new PatternLayout("%m%n")); + + log.info("Connecting to " + server); + ConnectionConfiguration jabberConfig = new ConnectionConfiguration(server); + jabberConfig.setSendPresence(false); + jabberConfig.setRosterLoadedAtLogin(false); + connection = new XMPPConnection(jabberConfig); + connection.connect(); + connection.login(user, password); + + chat = connection.getChatManager().createChat(recipient, new MessageListener() { + @Override + public void processMessage(Chat chat, Message message) { + // nothing + } + }); + } + + @Override + protected void append(LoggingEvent event) { + sendChat(layout.format(event)); + } + + @Override + public void close() { + log.info("Closing XMPP connection"); + connection.disconnect(); + } + + @Override + public boolean requiresLayout() { + return false; + } + + public void sendChat(String message) { + try { + chat.sendMessage(message); + } catch (XMPPException xe) { + log.error(xe.getMessage(), xe); + } + } + +} diff --git a/src/main/java/de/tilman/synctool/SyncTool.java b/src/main/java/de/tilman/synctool/SyncTool.java new file mode 100644 index 0000000..475e359 --- /dev/null +++ b/src/main/java/de/tilman/synctool/SyncTool.java @@ -0,0 +1,593 @@ +/* + * Copyright 2011 Tilman Walther + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package de.tilman.synctool; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.HashMap; + +import org.apache.commons.codec.digest.DigestUtils; +import org.apache.commons.io.FileUtils; +import org.apache.log4j.BasicConfigurator; +import org.apache.log4j.ConsoleAppender; +import org.apache.log4j.FileAppender; +import org.apache.log4j.Level; +import org.apache.log4j.Logger; +import org.apache.log4j.PatternLayout; +import org.apache.log4j.RollingFileAppender; +import org.jivesoftware.smack.XMPPException; + +import com.martiansoftware.jsap.FlaggedOption; +import com.martiansoftware.jsap.JSAP; +import com.martiansoftware.jsap.JSAPException; +import com.martiansoftware.jsap.JSAPResult; +import com.martiansoftware.jsap.Switch; +import com.martiansoftware.jsap.UnflaggedOption; + +import de.tilman.log4j.JabberAppender; + + +/** + * SyncTool synchronized two directories recursively. + * + * @author Tilman Walther + */ +public class SyncTool { + + private final static Logger log = Logger.getLogger(SyncTool.class); + + /** + * Defines the different possible operations for two files in the file tree + */ + private enum Operation { + COPY, COPYDESTINATION, DELETE, NONE + } + + private Connection connection; + private Statement statement; + + private ResultSet resultSet; + private PreparedStatement selectFileSql; + private PreparedStatement insertFileSql; + private PreparedStatement deleteFileSql; + + private boolean dryRun; + private boolean hashing; + private boolean silent; + private boolean ignoreDirAttribs; + + private long filesCompared; + private long dirsCompared; + private long dirsCopied; + private long dirsDeleted; + private long filesCopied; + private long filesDeleted; + + public SyncTool(JSAPResult config) { + + this.dryRun = config.getBoolean("dry-run"); + if (dryRun) + log.info("Performing dry-run, no changes to the file system"); + + this.silent = config.getBoolean("silent"); + if (silent) + log.info("Silent logging"); + + this.ignoreDirAttribs = config.getBoolean("ignore directory attributes"); + if (ignoreDirAttribs) + log.info("Ignoring directory attributes"); + + this.hashing = config.getBoolean("hashing"); + if (hashing) + log.info("Using MD5 hashes to compare files"); + + try { + log.info("Connecting to database \"" + config.getString("database file") + "\""); + Class.forName("org.h2.Driver"); + + connection = DriverManager.getConnection("jdbc:h2:file:" + config.getString("database file"), "sa", ""); + statement = connection.createStatement(); + + resultSet = connection.getMetaData().getTables(null, null, "%", new String[] { "TABLE" }); + if (!resultSet.next()) { + log.info("Creating database structure"); + statement.execute("CREATE CACHED TABLE Source (" + + "id INTEGER GENERATED BY DEFAULT AS IDENTITY (START WITH 1) PRIMARY KEY, " + + "path VARCHAR NOT NULL, " + + "lastSync TIMESTAMP NOT NULL, " + + "UNIQUE (path));"); + statement.execute("CREATE CACHED TABLE File (" + + "path VARCHAR NOT NULL, " + + "idSource INTEGER NOT NULL, " + + "FOREIGN KEY (idSource)" + + " REFERENCES Source(id)" + + " ON DELETE CASCADE);"); + statement.execute("CREATE INDEX IDX_ID_PATH ON File(path, idSource);"); + } + + } catch (Exception e) { + log.fatal(e.getMessage(), e); + System.exit(-1); + } + } + + /** + * Synchronizes two directories specified by their respective paths. + * + * @param srcPath the path to the source directory + * @param destPath the path to the destination directory + */ + public void sync(String srcPath, String destPath) { + + File srcRoot = new File(srcPath); + File destRoot = new File(destPath); + + if (!srcRoot.isDirectory()) { + log.fatal(srcRoot + " is not a directory"); + System.exit(-2); + } + if (!destRoot.isDirectory()) { + log.fatal(destRoot + " is not a directory"); + System.exit(-3); + } + + String canonicalSrcPath = null; + String canonicalDestPath; + + try { + canonicalSrcPath = srcRoot.getCanonicalPath(); + canonicalDestPath = destRoot.getCanonicalPath(); + if (canonicalSrcPath.equals(canonicalDestPath)) { + log.fatal("Source and destination point to the same directory: " + canonicalSrcPath); + System.exit(-4); + } + } catch (IOException ioe) { + log.fatal(ioe.getMessage(), ioe); + System.exit(-5); + } + + try { + resultSet = statement.executeQuery("SELECT * FROM Source WHERE path='" + canonicalSrcPath + "' LIMIT 1"); + + // check the database for the source directory + Integer sourceId = null; + if (resultSet.next()) { + sourceId = resultSet.getInt(1); + Timestamp lastSync = resultSet.getTimestamp(3); + log.info("Last sync for source path: " + lastSync); + } else { + log.info("Inserting new source path into database: " + canonicalSrcPath); + if (!dryRun) { + statement.executeUpdate("INSERT INTO Source (id, path, lastSync) VALUES (NULL, '" + canonicalSrcPath + + "', CURRENT_TIMESTAMP)"); + resultSet = statement.executeQuery("SELECT * FROM Source WHERE path='" + canonicalSrcPath + "' LIMIT 1"); + resultSet.next(); + sourceId = resultSet.getInt(1); + } else { + sourceId = -1; + } + } + + // prepare SQL statements + selectFileSql = connection.prepareCall("SELECT * FROM File WHERE path=? AND idSource=" + sourceId + " LIMIT 1"); + insertFileSql = connection.prepareCall("INSERT INTO File (idSource, path) VALUES (" + sourceId + ", ?)"); + deleteFileSql = connection.prepareCall("DELETE FROM File WHERE idSource=" + sourceId + " AND path=?"); + + filesCompared = 0; + dirsCompared = 0; + dirsCopied = 0; + dirsDeleted = 0; + filesCopied = 0; + filesDeleted = 0; + + log.info("Synchronizing " + srcRoot + " with " + destRoot); + recurse(srcRoot, destRoot); + + selectFileSql.close(); + insertFileSql.close(); + deleteFileSql.close(); + + log.info("Updating source entry in database"); + if (!dryRun) + statement.executeUpdate("UPDATE Source SET lastSync=CURRENT_TIMESTAMP WHERE id=" + sourceId); + + statement.execute("SHUTDOWN COMPACT"); + statement.close(); + connection.close(); + + log.info("Subdirectories compared: " + dirsCompared); + log.info(" Subdirectories copied: " + dirsCopied); + log.info(" Subdirectories deleted: " + dirsDeleted); + log.info("Files compared: " + filesCompared); + log.info(" Files copied: " + filesCopied); + log.info(" Files deleted: " + filesDeleted); + + } catch (SQLException e) { + log.fatal(e.getMessage(), e); + System.exit(-6); + } + + } + + + private File[] srcFiles; + private HashMap destMap; + + /** + * Synchronizes two directories recursively. + * + * @param srcDir the source directory + * @param destDir the destination directory + */ + private void recurse(File srcDir, File destDir) { + + log.debug(" get listing for source directory"); + srcFiles = srcDir.listFiles(); + destMap = new HashMap(); + log.debug(" get listing for destination directory"); + for (File file : destDir.listFiles()) { + destMap.put(file.getName(), file); + } + + ArrayList recurseList = new ArrayList(); + + try { + log.debug(" sync source side"); + for (int i = srcFiles.length - 1; i >= 0; i--) { + File srcFile = srcFiles[i]; + srcFiles[i] = null; + File destFile = destMap.remove(srcFile.getName()); + + // check synchronization history + log.debug(" get source history from database"); + selectFileSql.setString(1, srcFile.getCanonicalPath()); + resultSet = selectFileSql.executeQuery(); + boolean history = false; + if (resultSet.next()) + history = true; + + // determine what to do and do it + log.debug(" get operation"); + Operation operation = getOperation(srcFile, destFile, history); + log.debug(" synchronize"); + if (operation == Operation.COPYDESTINATION) + syncFileToDirectory(destFile, srcDir, Operation.COPY); + else + syncFileToDirectory(srcFile, destDir, operation); + + // if the file is a directory and has not been copied or + // deleted, add for recursion + if (srcFile.isDirectory() && operation == Operation.NONE) { + log.debug(" adding directory for recursion"); + if (destFile == null) { + destFile = new File(destDir, srcFile.getName()); + } + recurseList.add(new File[] { srcFile, destFile }); + } + } + + // opposite direction: process remaining files from destination + log.debug(" sync destination side"); + for (File destFile : destMap.values()) { + // check synchronization history + selectFileSql.setString(1, new File(srcDir, destFile.getName()).getCanonicalPath()); + log.debug(" get history"); + resultSet = selectFileSql.executeQuery(); + boolean history = false; + if (resultSet.next()) + history = true; + + log.debug(" synchronize"); + syncFileToDirectory(destFile, srcDir, getOperation(destFile, null, history)); + } + + // recurse all directories that have not been deleted or entirely + // copied + for (File[] recurseDir : recurseList) { + if (!silent) + log.info("Entering directory " + recurseDir[0]); + recurse(recurseDir[0], recurseDir[1]); + } + + } catch (Exception e) { + log.fatal(e.getMessage(), e); + System.exit(-7); + } + } + + /** + * Conducts the specified operation for the file. + * + * @param file the file to be processed + * @param directory the target directory + * @param operation the operation to be executed + */ + private void syncFileToDirectory(File file, File directory, Operation operation) { + + try { + if (operation == Operation.NONE) { + if (!silent) + log.info("No operation for " + file); + return; + } else if (operation == Operation.COPY) { + if (file.isDirectory()) { + log.info("Copying directory " + file); + if (!dryRun) + FileUtils.copyDirectoryToDirectory(file, directory); + dirsCopied++; + } else { + log.info("Copying file " + file); + if (!dryRun) + FileUtils.copyFileToDirectory(file, directory); + filesCopied++; + } + return; + } else if (operation == Operation.DELETE) { + if (file.isDirectory()) { + log.info("Deleting directory " + file); + if (!dryRun) + FileUtils.deleteDirectory(file); + dirsDeleted++; + } else { + log.info("Deleting file " + file); + if (!dryRun) + file.delete(); + filesDeleted++; + } + return; + } + } catch (IOException ioe) { + log.fatal(ioe.getMessage(), ioe); + System.exit(-8); + } + } + + /** + * Determines what to do with two files at the same place in the file tree + * on the source and the destination. This method also updates the database + * for the source file. + */ + private Operation getOperation(File srcFile, File destFile, boolean history) throws SQLException, IOException { + + if (destFile != null && destFile.exists()) { + + if (!history) { + insertFileSql.setString(1, srcFile.getCanonicalPath()); + if (!dryRun) + insertFileSql.execute(); + } + + if (srcFile.isDirectory()) { + dirsCompared++; + if (!dryRun && !ignoreDirAttribs) { + if (srcFile.lastModified() != destFile.lastModified() +// || srcFile.canExecute() != destFile.canExecute() +// || srcFile.canRead() != destFile.canRead() +// || srcFile.canWrite() != destFile.canWrite() + ) { + log.info("Setting attributes for " + destFile); + destFile.setLastModified(srcFile.lastModified()); +// destFile.setExecutable(srcFile.canExecute()); +// destFile.setReadable(srcFile.canRead()); +// destFile.setWritable(srcFile.canWrite()); + } + } + return Operation.NONE; + } + + filesCompared++; + + if (consideredEqual(srcFile, destFile)) + return Operation.NONE; + + if (srcFile.lastModified() > destFile.lastModified()) + return Operation.COPY; // copy source file + + return Operation.COPYDESTINATION; // copy destination file + } + + // if the file exists in the history, it has been deleted on the + // target side and should also be deleted on the source side + if (history) { + deleteFileSql.setString(1, srcFile.getCanonicalPath()); + if (!dryRun) + deleteFileSql.execute(); + return Operation.DELETE; + } + + // if the file is not present in the synchronization history, it + // has been added on the source side and should be copied + insertFileSql.setString(1, srcFile.getCanonicalPath()); + if (!dryRun) + insertFileSql.execute(); + return Operation.COPY; // copy source file + + } + + /** + * Determines whether two files at the same place in the file tree are + * considered to be equal under the given parameters. + * @throws IOException + * @throws FileNotFoundException + */ + private boolean consideredEqual(File src, File dest) throws FileNotFoundException, IOException { + // TODO add a certain amount to the source timestamp, if defined by parameter + // TODO allow a certain difference for the timestamps, if defined by parameter + if ((src.lastModified() == dest.lastModified()) && (src.length() == dest.length())) { + if (!hashing) + return true; + if (DigestUtils.md5Hex(new FileInputStream(src)).equals(DigestUtils.md5Hex(new FileInputStream(dest)))) + return true; + } + return false; + } + + + public static void main(String[] args) { + + BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d{ISO8601} - %m%n"))); + log.info("Starting SyncTool version 1.2.1"); + + JSAP jsap = new JSAP(); + + try { + UnflaggedOption sourceOption = new UnflaggedOption("source path").setStringParser(JSAP.STRING_PARSER).setRequired( + true); + sourceOption.setHelp("the source path"); + jsap.registerParameter(sourceOption); + + UnflaggedOption destinationOption = new UnflaggedOption("destination path").setStringParser(JSAP.STRING_PARSER) + .setRequired(true); + destinationOption.setHelp("the destination path"); + jsap.registerParameter(destinationOption); + + FlaggedOption dbFileOption = new FlaggedOption("database file").setStringParser(JSAP.STRING_PARSER).setLongFlag( + "dbfile").setShortFlag('f').setDefault("synctool"); + dbFileOption.setHelp("the path to the database file to use"); + jsap.registerParameter(dbFileOption); + + FlaggedOption logfileOption = new FlaggedOption("logfile").setStringParser(JSAP.STRING_PARSER) + .setLongFlag("logfile").setShortFlag('l'); + logfileOption.setHelp("the path for a logfile to write"); + jsap.registerParameter(logfileOption); + + FlaggedOption jabberAddress = new FlaggedOption("jabber address").setStringParser(JSAP.STRING_PARSER).setLongFlag( + "jabber").setShortFlag('j'); + jabberAddress.setHelp("send logging output as jabber message to the given address"); + jsap.registerParameter(jabberAddress); + + FlaggedOption jabberServer = new FlaggedOption("jabber server").setStringParser(JSAP.STRING_PARSER).setLongFlag( + "server").setShortFlag('r'); + jabberServer.setHelp("the jabber server to connect to"); + jsap.registerParameter(jabberServer); + + FlaggedOption jabberUser = new FlaggedOption("jabber user").setStringParser(JSAP.STRING_PARSER).setLongFlag( + "user").setShortFlag('u'); + jabberUser.setHelp("the jabber user name used for logging in to the server"); + jsap.registerParameter(jabberUser); + + FlaggedOption jabberPassword = new FlaggedOption("jabber password").setStringParser(JSAP.STRING_PARSER).setLongFlag( + "password").setShortFlag('p'); + jabberPassword.setHelp("the jabber password used for logging in to the server"); + jsap.registerParameter(jabberPassword); + + Switch dryRunSwitch = new Switch("dry-run").setLongFlag("dry-run").setShortFlag('d'); + dryRunSwitch.setHelp("perform a trial run with no changes made"); + jsap.registerParameter(dryRunSwitch); + + Switch hashingSwitch = new Switch("hashing").setLongFlag("hashing").setShortFlag('h'); + hashingSwitch.setHelp("generate MD5 file hashes for exact comparison"); + jsap.registerParameter(hashingSwitch); + + Switch rollingSwitch = new Switch("rolling-logfile").setLongFlag("rolling-logfile").setShortFlag('o'); + rollingSwitch.setHelp("generate a rolling logfile with a maximum size of 10 MB"); + jsap.registerParameter(rollingSwitch); + + Switch ignoreDirAttribsSwitch = new Switch("ignore directory attributes").setLongFlag("ignore-directory-attributes").setShortFlag('i'); + ignoreDirAttribsSwitch.setHelp("do not copy attributes for directories"); + jsap.registerParameter(ignoreDirAttribsSwitch); + + Switch silentSwitch = new Switch("silent").setLongFlag("silent").setShortFlag('s'); + silentSwitch.setHelp("do not print \"Entering directory\" and \"No operation\" messages"); + jsap.registerParameter(silentSwitch); + + FlaggedOption checkFileExists = new FlaggedOption("checkfile").setStringParser(JSAP.STRING_PARSER).setLongFlag( + "check-file-exists"); + checkFileExists.setHelp("perfom synchronization only if the given file exists"); + jsap.registerParameter(checkFileExists); + + Switch debugSwitch = new Switch("debug").setLongFlag("debug"); + debugSwitch.setHelp("print debug messages"); + jsap.registerParameter(debugSwitch); + + Switch helpSwitch = new Switch("help").setLongFlag("help").setShortFlag('?'); + helpSwitch.setHelp("print help and exit"); + jsap.registerParameter(helpSwitch); + + } catch (JSAPException e) { + log.fatal(e.getMessage()); + System.exit(-1002); + } + + JSAPResult config = jsap.parse(args); + + if (!config.success() || config.getBoolean("help")) { + for (java.util.Iterator errs = config.getErrorMessageIterator(); errs.hasNext();) { + log.error("Error: " + errs.next()); + } + + log.error("Usage: java -jar synctool.jar " + jsap.getUsage() + "\n\n" + jsap.getHelp()); + System.exit(-1003); + } + + if (config.getBoolean("debug")) { + log.setLevel(Level.DEBUG); + } + else { + log.setLevel(Level.INFO); + } + log.info("Log level set to " + log.getLevel()); + + if (config.getString("logfile") != null) { + try { + if (config.getBoolean("rolling-logfile")) { + BasicConfigurator.configure(new RollingFileAppender(new PatternLayout("%d{ISO8601} - %m%n"), config.getString("logfile"))); + log.info("Logging to rolling logfile " + config.getString("logfile")); + } + else { + BasicConfigurator.configure(new FileAppender(new PatternLayout("%d{ISO8601} - %m%n"), config.getString("logfile"))); + log.info("Logging to " + config.getString("logfile")); + } + } catch (IOException ioe) { + log.fatal(ioe.getMessage(), ioe); + System.exit(-1004); + } + } + + if (config.getString("jabber address") != null) { + try { + BasicConfigurator.configure(new JabberAppender(config.getString("jabber address"), config.getString("jabber server"), config.getString("jabber user"), config.getString("jabber password"))); + } catch (XMPPException xe) { + log.fatal(xe.getMessage(), xe); + System.exit(-1005); + } + log.info("Logging to " + config.getString("jabber address")); + } + + if (config.getString("checkfile") != null) { + if (new File(config.getString("checkfile")).exists() == false) { + log.error("The file " + config.getString("checkfile") + " does not exist. Stopping synchronization."); + System.exit(-1006); + } + } + + SyncTool syncTool = new SyncTool(config); + syncTool.sync(config.getString("source path"), config.getString("destination path")); + } +} diff --git a/src/test/java/de/tilman/sync/AppTest.java b/src/test/java/de/tilman/sync/AppTest.java new file mode 100644 index 0000000..b64729f --- /dev/null +++ b/src/test/java/de/tilman/sync/AppTest.java @@ -0,0 +1,38 @@ +package de.tilman.sync; + +import junit.framework.Test; +import junit.framework.TestCase; +import junit.framework.TestSuite; + +/** + * Unit test for simple App. + */ +public class AppTest + extends TestCase +{ + /** + * Create the test case + * + * @param testName name of the test case + */ + public AppTest( String testName ) + { + super( testName ); + } + + /** + * @return the suite of tests being tested + */ + public static Test suite() + { + return new TestSuite( AppTest.class ); + } + + /** + * Rigourous Test :-) + */ + public void testApp() + { + assertTrue( true ); + } +} diff --git a/target/maven-archiver/pom.properties b/target/maven-archiver/pom.properties new file mode 100644 index 0000000..6001367 --- /dev/null +++ b/target/maven-archiver/pom.properties @@ -0,0 +1,5 @@ +#Generated by Maven +#Mon Mar 21 17:33:34 CET 2011 +version=1.0.8 +groupId=SyncTool +artifactId=synctool diff --git a/target/surefire-reports/TEST-de.tilman.sync.AppTest.xml b/target/surefire-reports/TEST-de.tilman.sync.AppTest.xml new file mode 100644 index 0000000..3903456 --- /dev/null +++ b/target/surefire-reports/TEST-de.tilman.sync.AppTest.xml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/target/surefire-reports/de.tilman.sync.AppTest.txt b/target/surefire-reports/de.tilman.sync.AppTest.txt new file mode 100644 index 0000000..d5bb740 --- /dev/null +++ b/target/surefire-reports/de.tilman.sync.AppTest.txt @@ -0,0 +1,4 @@ +------------------------------------------------------------------------------- +Test set: de.tilman.sync.AppTest +------------------------------------------------------------------------------- +Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.008 sec