diff --git a/README.md b/README.md index 349bf4f..c06f5ab 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,37 @@ These dedicated folders are designed to keep the intermediate data files that are in fact the patches or bundles of Git or other VCS system. This bridge is duplex - so that you can exchange data in both directions. +Contents +-------- + +1. [The Short Explanation](#the-short-explanation) +2. [How It Works In A Nutshell](#how-it-works-in-a-nutshell) + 1. [The Overall Diagram](#the-overall-diagram) +3. [Build And Run](#build-and-run) + 1. [Prerequisites](#prerequisites) + 2. [Build](#build) + 3. [Run](#run) + 4. [Notices](#notices) +4. [Usage](#usage) + 1. [Configuration file](#configuration-file) + 1. [EWS Settings](#ews-settings) + 2. [Test bridge on one node](#test-bridge-on-one-node) + 3. [Both sides use the same EWS server](#both-sides-use-the-same-ews-server) + 4. [Each side uses its own EWS server](#each-side-uses-its-own-ews-server) + 2. [Git Bundle Mode](#git-bundle-mode) + 1. [Case 1. Existing repo on Side 1 and new empty repo on Side 2 + ](#case-1-existing-repo-on-side-1-and-new-empty-repo-on-side-2) + 2. [Case 2. Changes are committed on Side 1, need to be pushed to Side 2 + ](#case-2-changes-are-committed-on-side-1-need-to-be-pushed-to-side-2) + 3. [Git Email Mode](#git-email-mode) + 1. [Get full patch set from the beginning](#get-full-patch-set-from-the-beginning) + 2. [Get partial patch set from tagged commit](#get-partial-patch-set-from-tagged-commit) + 3. [Restore the patch set](#restore-the-patch-set) + 4. [Post-Receive Script](#post-receive-script) + 5. [Tune Logging](#tune-logging) +5. [References](#references) +6. [TODO](#todo) + The Short Explanation --------------------- @@ -24,7 +55,7 @@ initial conditions: non-detectable (encrypted) attachments. * Only files (not sub-folders) are synchronized between computers. That's by design limitation, b/c application's `inbox` and `outbox` purpose is to - temporarily keep VCS (Git) patches or bundles that in fact is plain list, so + temporarily keep VCS ([Git]) patches or bundles that in fact is plain list, so that any kind of file hierarchy support is business of VCS. How It Works In A Nutshell @@ -54,7 +85,7 @@ Both sides have symmetric settings and work in absolutely the same manner. So the main flow may look like that: 1. User of "Side 1" puts file or files into `outbox` folder (these files are - intended to be the result of Git command - see the **"Usage"** section below + intended to be the result of [Git] command - see the **"Usage"** section below for details). 2. App on "Side 1" detects that new files are appeared in `outbox` and creates new Email message(s) where attaches these files (packed and/or encrypted if @@ -64,7 +95,7 @@ So the main flow may look like that: 4. App on "Side 2" detects that new email is available, so receives it and processes to obtain files from attachments and puts these files into `inbox` folder in its original representation (decrypted and/or unpacked). -5. User of "Side 2" runs appropriate commands (e.g. Git) to apply new changes +5. User of "Side 2" runs appropriate commands (e.g. [Git]) to apply new changes represented by these files, and cleans up the `inbox` folder. The last point may be automated as well - see **"Post-Receive Script"** section @@ -75,12 +106,18 @@ Build And Run ### Prerequisites ### -To build and run this application you need: +To build this application you need: -* Java 7+; -* Maven 3+; +* JDK 7+; +* [Maven](https://maven.apache.org/) 3+; * Internet connection. +To run: + +* JRE/JDK 7+; +* [Git]; +* If OS is Windows - [Cygwin] - optional but recommended. + ### Build ### This project is Maven-driven, so all what you need to do to build it is to run @@ -89,19 +126,17 @@ the following command: $ mvn clean package ### Run ### - + If build is successful, you can run the resulting JAR file as standalone Java app like that: - $ java -jar target/email-bridge-X.X.X-standalone.jar - -Where `X.X.X` is current version number. + $ java -jar target/email-bridge-0.1.2-standalone.jar The invocation w/o arguments will show an error that proper configuration file is required. Just in case - you can get the short help about supported command line arguments by specifying `-h` option: - $ java -jar target/email-bridge-X.X.X-standalone.jar -h + $ java -jar target/email-bridge-0.1.2-standalone.jar -h ### Notices ### @@ -457,12 +492,14 @@ References 3. [Prepare patches for e-mail submission][git-format-patch] 4. [Apply a series of patches from a mailbox][git-am] 5. [Cygwin Project][Cygwin] +6. [Git VCS][Git] [ews-java-api]: https://github.com/OfficeDev/ews-java-api [git-bundle]: http://git-scm.com/docs/git-bundle [git-format-patch]: http://git-scm.com/docs/git-format-patch [git-am]: http://git-scm.com/docs/git-am [Cygwin]: http://cygwin.org/ +[Git]: https://git-scm.com/downloads TODO ---- diff --git a/data/config-template.properties b/data/config-template.properties index 6092f8b..05c96d9 100644 --- a/data/config-template.properties +++ b/data/config-template.properties @@ -162,3 +162,10 @@ email.recipients.to = # If file size is bigger, it will be split to several parts. # Default value is 5. #email.attach.max.size = + +# Optional path to PID file. +# If PID file is specified but cannot be created/rewritten then application stops with error. +#pid.file = + +# Whether to keep or not the PID file on application finish. Default value is "false". +#pid.file.keep = \ No newline at end of file diff --git a/pom.xml b/pom.xml index b76c0d7..bdce6a1 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ org.exchange.git email-bridge - 0.1.1 + 0.1.2 jar email-bridge diff --git a/src/main/java/org/mail/bridge/AbstractMonitor.java b/src/main/java/org/mail/bridge/AbstractMonitor.java index 501f3b6..840f4a2 100644 --- a/src/main/java/org/mail/bridge/AbstractMonitor.java +++ b/src/main/java/org/mail/bridge/AbstractMonitor.java @@ -33,11 +33,12 @@ /** * @author Maksym Dominichenko */ +@SuppressWarnings("UnusedReturnValue") public abstract class AbstractMonitor { private final Map>, List>> callbacks = new HashMap<>(); - protected AbstractMonitor addCallback(Class> messageClass, MonitorCallback callback) { + AbstractMonitor addCallback(Class> messageClass, MonitorCallback callback) { if (callback != null) { List> callbackList = callbacks.get(messageClass); if (callbackList == null) { @@ -50,7 +51,7 @@ protected AbstractMonitor addCallback(Class> messageCla } @SuppressWarnings({"unchecked", "SuspiciousMethodCalls"}) - protected void postMessage(Message message) { + void postMessage(Message message) { List callbackList = Utils.ensureEmpty(callbacks.get(message.getClass())); for (MonitorCallback callback : (List>) callbackList) callback.onMessage(message); diff --git a/src/main/java/org/mail/bridge/Config.java b/src/main/java/org/mail/bridge/Config.java index 4a088f0..f96ddb0 100644 --- a/src/main/java/org/mail/bridge/Config.java +++ b/src/main/java/org/mail/bridge/Config.java @@ -82,6 +82,9 @@ public class Config { private final String emailAttachExtEnc; private final int emailAttachMaxSize; + private final String pidFile; + private final boolean pidFileKeep; + public Config(String propertiesFileName) throws IOException { Properties config = new Properties(); config.load(new FileInputStream(propertiesFileName)); @@ -165,133 +168,145 @@ public Config(String propertiesFileName) throws IOException { emailAttachExtEnc = checkExt(s.isEmpty() ? ".enc" : s); s = config.getProperty("email.attach.max.size", ""); emailAttachMaxSize = s.isEmpty() ? 5 : Integer.parseInt(s); + + pidFile = config.getProperty("pid.file", ""); + s = config.getProperty("pid.file.keep", ""); + pidFileKeep = !s.isEmpty() && Boolean.parseBoolean(s); } - public String getEwsEmail() { + String getEwsEmail() { return ewsEmail; } - public String getEwsDomain() { + String getEwsDomain() { return ewsDomain; } - public String getEwsUsername() { + String getEwsUsername() { return ewsUsername; } - public String getEwsPassword() { + String getEwsPassword() { return ewsPassword; } - public String getEwsServer() { + String getEwsServer() { return ewsServer; } - public int getEwsViewSize() { + int getEwsViewSize() { return ewsViewSize; } - public int getEwsSubscriptionLifetime() { + int getEwsSubscriptionLifetime() { return ewsSubscriptionLifetime; } - public String getProxyHost() { + String getProxyHost() { return proxyHost; } - public int getProxyPort() { + int getProxyPort() { return proxyPort; } - public String getProxyUsername() { + String getProxyUsername() { return proxyUsername; } - public String getProxyPassword() { + String getProxyPassword() { return proxyPassword; } - public String getProxyDomain() { + String getProxyDomain() { return proxyDomain; } - public String getOutboxFolder() { + String getOutboxFolder() { return outboxFolder; } - public boolean isOutboxCleanup() { + boolean isOutboxCleanup() { return outboxCleanup; } - public String getOutboxFileRegexp() { + String getOutboxFileRegexp() { return outboxFileRegexp; } - public String getInboxFolder() { + String getInboxFolder() { return inboxFolder; } - public String getInboxScript() { + String getInboxScript() { return inboxScript; } - public int getInboxScriptStopCode() { + int getInboxScriptStopCode() { return inboxScriptStopCode; } - public boolean isEmailInboxCleanup() { + boolean isEmailInboxCleanup() { return emailInboxCleanup; } - public String getEmailTagIncoming() { + String getEmailTagIncoming() { return emailTagIncoming; } - public String getEmailTagOutgoing() { + String getEmailTagOutgoing() { return emailTagOutgoing; } - public MessageFormat getEmailSubjectFormat() { + MessageFormat getEmailSubjectFormat() { return emailSubjectFormat; } - public MessageFormat getEmailBodyFormat() { + MessageFormat getEmailBodyFormat() { return emailBodyFormat; } - public String[] getEmailRecipientsTo() { + String[] getEmailRecipientsTo() { return emailRecipientsTo; } - public String[] getEmailRecipientsCc() { + String[] getEmailRecipientsCc() { return emailRecipientsCc; } - public String[] getEmailRecipientsBcc() { + String[] getEmailRecipientsBcc() { return emailRecipientsBcc; } - public String getEmailAttachPassword() { + String getEmailAttachPassword() { return emailAttachPassword; } - public boolean isEmailAttachGzip() { + boolean isEmailAttachGzip() { return emailAttachGzip; } - public String getEmailAttachExtGzip() { + String getEmailAttachExtGzip() { return emailAttachExtGzip; } - public String getEmailAttachExtEnc() { + String getEmailAttachExtEnc() { return emailAttachExtEnc; } - public int getEmailAttachMaxSize() { + int getEmailAttachMaxSize() { return emailAttachMaxSize; } - public Map asEnvironmentMap() { + String getPidFile() { + return pidFile; + } + + boolean isPidFileKeep() { + return pidFileKeep; + } + + Map asEnvironmentMap() { Map result = new HashMap<>(); result.put("EWS_EMAIL", ewsEmail); result.put("EWS_DOMAIN", ewsDomain); @@ -324,6 +339,8 @@ public Map asEnvironmentMap() { result.put("EMAIL_ATTACH_EXT_GZIP", emailAttachExtGzip); result.put("EMAIL_ATTACH_EXT_ENC", emailAttachExtEnc); result.put("EMAIL_ATTACH_MAX_SIZE", "" + emailAttachMaxSize); + result.put("PID_FILE", pidFile); + result.put("PID_FILE_KEEP", "" + pidFileKeep); return result; } @@ -367,6 +384,8 @@ public String toString() { ",\n\temailAttachExtGzip='" + emailAttachExtGzip + '\'' + ",\n\temailAttachExtEnc='" + emailAttachExtEnc + '\'' + ",\n\temailAttachMaxSize=" + emailAttachMaxSize + + ",\n\tpidFile='" + pidFile + '\'' + + ",\n\tpidFileKeep=" + pidFileKeep + '}'; } } diff --git a/src/main/java/org/mail/bridge/ExchangeMonitor.java b/src/main/java/org/mail/bridge/ExchangeMonitor.java index b6b9f71..4f838ae 100644 --- a/src/main/java/org/mail/bridge/ExchangeMonitor.java +++ b/src/main/java/org/mail/bridge/ExchangeMonitor.java @@ -77,20 +77,20 @@ public class ExchangeMonitor extends AbstractMonitor implements private static final Pattern RE_ZIP_VOL = compile("^(" + RE_UUID + ")_(\\d+)\\.z\\d{2}$", CASE_INSENSITIVE); private static final String ZIP_EXT = ".z00"; - public static class NewMailMessage extends Message> { - public NewMailMessage(List emails) { + static class NewMailMessage extends Message> { + NewMailMessage(List emails) { super(emails); } } - public static class ReopenMonitorMessage extends Message { - public ReopenMonitorMessage() { + static class ReopenMonitorMessage extends Message { + ReopenMonitorMessage() { super(null); } } - public static class NewIncomingFilesMessage extends Message> { - public NewIncomingFilesMessage(List files) { + static class NewIncomingFilesMessage extends Message> { + NewIncomingFilesMessage(List files) { super(files); } } @@ -98,7 +98,7 @@ public NewIncomingFilesMessage(List files) { private final Config config; private ExchangeService service; - public ExchangeMonitor(Config config) { + ExchangeMonitor(Config config) { this.config = config; LOG.debug("Instantiated"); } @@ -130,19 +130,19 @@ public boolean autodiscoverRedirectionUrlValidationCallback(String redirectionUr } } - public ExchangeMonitor addStopCallback(MonitorCallback callback) { + ExchangeMonitor addStopCallback(MonitorCallback callback) { return (ExchangeMonitor) addCallback(Main.StopMessage.class, callback); } - public ExchangeMonitor addNewMailCallback(MonitorCallback> callback) { + ExchangeMonitor addNewMailCallback(MonitorCallback> callback) { return (ExchangeMonitor) addCallback(NewMailMessage.class, callback); } - public ExchangeMonitor addReopenMonitorCallback(MonitorCallback callback) { + ExchangeMonitor addReopenMonitorCallback(MonitorCallback callback) { return (ExchangeMonitor) addCallback(ReopenMonitorMessage.class, callback); } - public ExchangeMonitor addIncomingFilesReadyCallback(MonitorCallback> callback) { + ExchangeMonitor addIncomingFilesReadyCallback(MonitorCallback> callback) { return (ExchangeMonitor) addCallback(NewIncomingFilesMessage.class, callback); } @@ -375,7 +375,7 @@ public void subscriptionErrorDelegate(Object sender, SubscriptionErrorEventArgs postMessage(new ReopenMonitorMessage()); } - public synchronized void sendFiles(List files) { + synchronized void sendFiles(List files) { if (Utils.isEmpty(files)) return; LOG.info("Sending files '{}'", files); @@ -558,7 +558,7 @@ private void removeTempDir(File dir) { else LOG.error("Cannot remove temporary folder '{}'", dir.getAbsolutePath()); } - public synchronized void processNewMail(List newMailsIds) { + synchronized void processNewMail(List newMailsIds) { LOG.info("Start new mail processing - {} message(s)", newMailsIds.size()); try { ServiceResponseCollection responses = diff --git a/src/main/java/org/mail/bridge/FolderMonitor.java b/src/main/java/org/mail/bridge/FolderMonitor.java index 6733b52..a9c49e0 100644 --- a/src/main/java/org/mail/bridge/FolderMonitor.java +++ b/src/main/java/org/mail/bridge/FolderMonitor.java @@ -48,8 +48,8 @@ public class FolderMonitor extends AbstractMonitor implements Runnable { private static final long NEW_FILES_PROCESS_DELAY = 1000; private static final int SCRIPT_TIMEOUT = 60000; - public static class SendFileMessage extends Message> { - public SendFileMessage(List data) { + static class SendFileMessage extends Message> { + SendFileMessage(List data) { super(data); } } @@ -84,7 +84,7 @@ public boolean accept(File file) { } }; - public FolderMonitor(Config config) throws IOException { + FolderMonitor(Config config) throws IOException { this.config = config; outboxFolder = new File(config.getOutboxFolder()); if (outboxFolder.exists()) { @@ -97,11 +97,11 @@ else if (!outboxFolder.canRead() || !outboxFolder.canWrite()) LOG.debug("Instantiated"); } - public FolderMonitor addStopCallback(MonitorCallback callback) { + FolderMonitor addStopCallback(MonitorCallback callback) { return (FolderMonitor) addCallback(Main.StopMessage.class, callback); } - public FolderMonitor addSendFileCallback(MonitorCallback> callback) { + FolderMonitor addSendFileCallback(MonitorCallback> callback) { return (FolderMonitor) addCallback(SendFileMessage.class, callback); } @@ -111,7 +111,7 @@ private void processFiles(List files) throws IOException { postMessage(new SendFileMessage(files)); } - public synchronized void runScriptAgainstReceivedFiles(List inboxFiles) { + synchronized void runScriptAgainstReceivedFiles(List inboxFiles) { if (config.getInboxScript().isEmpty() || Utils.isEmpty(inboxFiles)) return; LOG.debug("Run script '{}' against files {}", config.getInboxScript(), inboxFiles); ByteArrayOutputStream out = new ByteArrayOutputStream(); diff --git a/src/main/java/org/mail/bridge/Main.java b/src/main/java/org/mail/bridge/Main.java index d31acbb..1015b81 100644 --- a/src/main/java/org/mail/bridge/Main.java +++ b/src/main/java/org/mail/bridge/Main.java @@ -31,7 +31,9 @@ import org.slf4j.LoggerFactory; import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.util.List; import java.util.concurrent.ConcurrentLinkedDeque; @@ -42,8 +44,8 @@ public class Main implements Runnable { private static final Logger LOG = LoggerFactory.getLogger(Main.class); - public static class StopMessage extends Message { - public StopMessage(String stopMessage) { + static class StopMessage extends Message { + StopMessage(String stopMessage) { super(stopMessage); } } @@ -51,6 +53,7 @@ public StopMessage(String stopMessage) { private final FolderMonitor folderMonitor; private final ExchangeMonitor exchangeMonitor; private final ConcurrentLinkedDeque> messages = new ConcurrentLinkedDeque<>(); + private final File pidFile; public static void main(String[] args) throws Exception { ArgumentParser parser = ArgumentParsers.newArgumentParser(Main.class.getSimpleName()); @@ -63,7 +66,7 @@ public static void main(String[] args) throws Exception { new Main(config); } - Main(Config config) throws IOException { + private Main(Config config) throws IOException { folderMonitor = new FolderMonitor(config) .addStopCallback(new MonitorCallback() { @Override @@ -102,6 +105,11 @@ public void onMessage(Message message) { postMessage(message); } }); + pidFile = config.getPidFile().isEmpty() ? null : new File(config.getPidFile()); + if (pidFile != null) { + if (pidFile.exists() && pidFile.delete()) LOG.debug("Old PID file was removed"); + if (!config.isPidFileKeep()) pidFile.deleteOnExit(); + } new Thread(this, Main.class.getSimpleName()).start(); } @@ -111,6 +119,23 @@ private synchronized void postMessage(Message message) { @Override public void run() { + if (pidFile != null) { + long pid; + try { + pid = Long.parseLong(ManagementFactory.getRuntimeMXBean().getName().split("@", 2)[0]); + } catch (Exception e) { + LOG.error("Cannot retrieve the PID of current process", e); + return; + } + try (FileOutputStream os = new FileOutputStream(pidFile)) { + os.write(String.valueOf(pid).getBytes()); + os.flush(); + } catch (Exception e) { + LOG.error("Cannot cannot write PID file", e); + return; + } + } + exchangeMonitor.scan().monitor(); folderMonitor.scan().monitor(); while (true) { @@ -141,5 +166,7 @@ else if (message instanceof StopMessage) { break; } } + + } } diff --git a/src/main/java/org/mail/bridge/Message.java b/src/main/java/org/mail/bridge/Message.java index e82e3f3..589b5cc 100644 --- a/src/main/java/org/mail/bridge/Message.java +++ b/src/main/java/org/mail/bridge/Message.java @@ -30,11 +30,11 @@ public class Message { private final T data; - public Message(T data) { + Message(T data) { this.data = data; } - public T getData() { + T getData() { return data; } diff --git a/src/main/java/org/mail/bridge/util/EncryptUtil.java b/src/main/java/org/mail/bridge/util/EncryptUtil.java index c03a507..64797ce 100644 --- a/src/main/java/org/mail/bridge/util/EncryptUtil.java +++ b/src/main/java/org/mail/bridge/util/EncryptUtil.java @@ -100,9 +100,10 @@ public static void decrypt(String password, InputStream cipherData, OutputStream } } - public static void gzipEncrypt(String password, - final InputStream clearData, - OutputStream cipherData) throws IOException { + public static void gzipEncrypt( + String password, + final InputStream clearData, + OutputStream cipherData) throws IOException { final PipedOutputStream output = new PipedOutputStream(); final PipedInputStream input = new PipedInputStream(output); final Thread thread = new Thread(new Runnable() { @@ -120,9 +121,10 @@ public void run() { encrypt(password, input, cipherData); } - public static void decryptGunzip(final String password, - final InputStream cipherData, - OutputStream clearData) throws IOException { + public static void decryptGunzip( + final String password, + final InputStream cipherData, + OutputStream clearData) throws IOException { final PipedOutputStream output = new PipedOutputStream(); final PipedInputStream input = new PipedInputStream(output); final Thread thread = new Thread(new Runnable() { diff --git a/src/main/java/org/mail/bridge/util/Utils.java b/src/main/java/org/mail/bridge/util/Utils.java index fe7cdbb..ee5a98f 100644 --- a/src/main/java/org/mail/bridge/util/Utils.java +++ b/src/main/java/org/mail/bridge/util/Utils.java @@ -29,7 +29,7 @@ /** * @author Maksym Dominichenko */ -@SuppressWarnings("unused") +@SuppressWarnings({"unused", "WeakerAccess", "SameParameterValue"}) public class Utils { public static final Comparator LAST_MODIFIED_COMPARATOR = new Comparator() {