From c0dc38dd9f2cf40af9f06f5b984b53ce4f3610be Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sun, 27 Apr 2014 15:25:44 +0200 Subject: [PATCH] Squashed 'core/' changes from 1825f5d..af6033d af6033d Edit/add help texts 98bc55f Force commit to trigger Travis c6c816b Merge branch 'develop' into release/0.1.2-alpha 51caad0 Merge Vadmin's bugfixes; fixes #118, closes #120 4ac8548 Merge branch 'fileHandleLeak' of https://github.com/vadimpanin/syncany into develop 7a79b45 Merge branch 'develop' into release/0.1.2-alpha afbd7d6 file handle leak fix (#118) 8f00bb6 Fix broken log test e987c28 Update CHANGELOG.md 2bf221e Update CHANGELOG.md 94a7f62 Fix Windows plugin JAR deletion; fixes #113, fixes #117 d5dff96 plugin purgefile on windows: batch commands moved to gradlew generator e717d33 plugin purgefile on windows: more simple approach bf58e6c Merge remote-tracking branch 'upstream/develop' into develop a49af43 Implement log file rotation 4*25MB; relates to #116 7276594 Merge develop f6107f3 Merge branch 'release/0.1.2-alpha' of github.com:binwiederhier/syncany into release/0.1.2-alpha 03a0812 Merge branch 'develop' of github.com:binwiederhier/syncany into develop 6499045 Code prettification 9343f3b Update CHANGELOG.md fa47631 Rename global config to user config, write demo config file, closes #109 417564f Add system properties configuration possibility in GlobalConfig. Relates to #109 5e7ed49 Immediately sync again if local or remote notification received; don't swallow notifications; fixes #88 6c9ab3e Merge branch 'master' of https://github.com/vadimpanin/syncany into develop ef0f27c Update README.md 53c11e7 Update CHANGELOG.md 999ded1 Pick random machine name; fixes #114 27379a6 Plugin not supported stack trace issue fixed. Fixes #111 a2079de Adding contributors link 6c7e76c Update CHANGELOG.md 55aae9f Added plugin compatibility check. Closes #104 3e343bc Merge Vadim's wildcard ignore changes 32cb98e Merge branch 'wildcardsToRegexps' of https://github.com/vadimpanin/syncany into develop a894ae2 fix and test for ? mark in wildcards 4051a76 styling and test for ignoring files using wildcards af0d0ce JavaDoc for new TransferManager interface 5fdf342 autoconverting wildcards to regexps 1e82ea4 Clear semantic of TM.test() 16c88ce Adjust Exceptions fa40d5a Potentially fix path issue under Windows #107 7714536 Merge branch 'release/0.1.1-alpha' d1562d0 Merge branch 'release/0.1.0-alpha' git-subtree-dir: core git-subtree-split: af6033db6a9bf3fc8f9c851a952cd9aa005f7bf0 --- CHANGELOG.md | 51 ++++---- README.md | 22 ++-- syncany-cli/build.gradle | 15 ++- .../org/syncany/cli/AbstractInitCommand.java | 14 +-- .../org/syncany/cli/CommandLineClient.java | 37 +++--- .../main/resources/help/cmd/help.connect.skel | 44 +++++++ syncany-cli/src/main/resources/help/help.skel | 1 + .../syncany/tests/cli/StatusCommandTest.java | 2 +- syncany-lib/build.gradle | 2 +- .../src/main/java/org/syncany/Client.java | 32 ++--- .../java/org/syncany/chunk/ZipMultiChunk.java | 9 +- .../java/org/syncany/config/ConfigHelper.java | 16 ++- .../java/org/syncany/config/IgnoredFiles.java | 37 +++++- .../java/org/syncany/config/UserConfig.java | 109 ++++++++++++++++++ .../org/syncany/config/to/UserConfigTO.java | 61 ++++++++++ .../plugins/AbstractTransferManager.java | 12 +- .../connection/plugins/StorageTestResult.java | 17 ++- .../connection/plugins/TransferManager.java | 74 ++++++++---- .../plugins/local/LocalTransferManager.java | 28 +++-- .../actions/FileCreatingFileSystemAction.java | 3 + .../operations/plugin/PluginOperation.java | 53 ++++++++- .../operations/watch/WatchOperation.java | 34 ++++-- .../scenarios/IgnoredFileScenarioTest.java | 38 ++++++ 23 files changed, 566 insertions(+), 145 deletions(-) create mode 100644 syncany-cli/src/main/resources/help/cmd/help.connect.skel create mode 100644 syncany-lib/src/main/java/org/syncany/config/UserConfig.java create mode 100644 syncany-lib/src/main/java/org/syncany/config/to/UserConfigTO.java diff --git a/CHANGELOG.md b/CHANGELOG.md index c7db336e..ac798334 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,10 +3,34 @@ Change Log ### Release 0.1.2-alpha (Date: tbd.) -- Not released yet. - +- Developer/alpha release (**NOT FOR PRODUCTION USE!**) +- Features: + + Extracted non-core plugins, allow easy plugin installation through + `sy plugin (list|install|remove)` #26/#104 + - Shipped plugins now only 'local' + - Installable plugins: + [FTP](https://github.com/syncany/syncany-plugin-ftp), + [SFTP](https://github.com/syncany/syncany-plugin-sftp) (no host checking), + [WebDAV](https://github.com/syncany/syncany-plugin-webdav) (HTTP only), + [Amazon S3](https://github.com/syncany/syncany-plugin-s3) + + Ignore files using wildcards in .syignore (e.g. *.bak, *.r??) #108 + + Added Arch Linux 'syncany-git' package #99 + + Allow speicifying HTTP(S)/WebDAV proxy and other global system properties #109 +- Bugfixes + + Fix semantic in TransferManager `test()` (incl. all plugins) #103/#102 + + WebDAV plugin fix to create "multichunks"/"databases" folder #110 + + Fix "Plugin not supported" stack trace #111 + + Windows build script fix for "Could not normalize path" #107 + + Fix database file name leak of username and hostname #114 + + Check plugin compatibility before installing (check appMinVersion) #104 + + Don't ignore local/remote notifications if sync already running #88 + + Uninstall plugins on Windows (JAR locked) #113/#117 + + Rotate logs to max. 4x25 MB #116 + + Fix multichunk resource close issue #118/#120 + ### Release 0.1.1-alpha (Date: 14 Apr 2014) +- Developer/alpha release (**NOT FOR PRODUCTION USE!**) - Features: + Ignoring files using .syignore file #66/#77 + Arch Linux package support; release version #80 and git version #99 @@ -14,32 +38,17 @@ Change Log - Windows-specific: + Add Syncany binaries to PATH environment variable during setup #84/#91 + Fixed HSQLDB-path issue #98 -- Bugfixes +- Bugfixes: + Timezone fix in tests #78/#90 + Reference issue "Cannot determine file content for checksum" #92/#94 + Atomic 'init' command (rollback on failure) #95/#96 -- Other things +- Other things: + Tests for 'connect' command + Tests for .syignore ### Release 0.1.0-alpha (Date: 30 March 2014) -- First developer/alpha release (NOT FOR PRODUCTION USE!) +- First developer/alpha release (**NOT FOR PRODUCTION USE!**) - Command line interface (CLI) with commands + init: initialize local folder and remote repository - + connect: connect to an existing remote repository - + up: index and upload local files - + down: download changes and apply locally - + status: list local changes - + ls-remote: list remote changes - + watch: watches local dir, subscribes to pub/sub, and calls down/up - command in a set interval - + restore: restores a given set of files (experimental) - + log: Outputs formatted file histories (experimental) - + genlink: Generates syncany:// links to share - + cleanup: Deletes old file versions and frees remote space -- Storage plugins: - + Local: Allows to store repository files in a local/mounted folder - + FTP: Allows the use of an FTP folder as repository - + WebDAV: Allows using a WebDAV folder as repository (currently no HTTPS) - + + connect diff --git a/README.md b/README.md index 280f8d1f..ff374b92 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ Syncany [![Build Status](https://travis-ci.org/binwiederhier/syncany.png?branch= Syncany is an open-source cloud storage and filesharing application. It allows users to backup and share certain folders of their workstations using any kind -of storage, e.g. FTP, Amazon S3 or Google Storage. +of storage, e.g. FTP, SFTP, WebDAV and Amazon S3. While the basic idea is similar to Dropbox, Syncany is open-source and additionally provides data encryption and more flexibility in @@ -31,7 +31,7 @@ Download and install Syncany You can download the current binary packages and installers from the [releases page](https://github.com/binwiederhier/syncany/releases), or from the Syncany [download site](http://syncany.org/dist/). **Please be aware that this is still ALPHA code! Do not use it for important files.** **Latest release:** -Syncany 0.1.1-alpha, 14 April 2014, [[tar.gz]](https://syncany.org/dist/syncany-0.1.1-alpha.tar.gz) [[zip]](https://syncany.org/dist/syncany-0.1.1-alpha.zip) [[deb]](https://syncany.org/dist/syncany_0.1.1-alpha_all.deb) [[exe]](https://syncany.org/dist/syncany-0.1.1-alpha.exe) +Syncany 0.1.2-alpha, 27 April 2014, [[tar.gz]](https://syncany.org/dist/releases/syncany-0.1.2-alpha.tar.gz) [[zip]](https://syncany.org/dist/releases/syncany-0.1.2-alpha.zip) [[deb]](https://syncany.org/dist/releases/syncany_0.1.2-alpha_all.deb) [[exe]](https://syncany.org/dist/releases/syncany-0.1.2-alpha.exe) Quick [install and usage instructions](https://github.com/binwiederhier/syncany/wiki/CLI-quick-howto) can be found in the wiki. If you like it a bit more detailed, [there's lots more you can explore](https://github.com/binwiederhier/syncany/wiki). @@ -43,18 +43,24 @@ Sample usage: Try Syncany Usage is pretty similar to a version control system. If you have used Git or SVN, it should feel a lot alike. -**1. Initialize a local directory** +**1. Choose and install a storage plugin** +First choose the storage backend you'd like to use by doing `sy plugin list` and then `sy plugin install`. As of today, we've implemented plugins for [FTP](https://github.com/syncany/syncany-plugin-ftp), [SFTP](https://github.com/syncany/syncany-plugin-sftp), [WebDAV](https://github.com/syncany/syncany-plugin-webdav) and [Amazon S3](https://github.com/syncany/syncany-plugin-s3). For this example, we'll install the FTP plugin: +``` +$ sy plugin install ftp +``` + +**2. Initialize a local directory** ``` $ sy init -Choose a storage plugin. Available plugins are: ftp, local, webdav +Choose a storage plugin. Available plugins are: ftp, local, webdav, s3, sftp Plugin: ftp Connection details for FTP connection: - Hostname: example.com - Username: ftpuser - Password (not displayed): -- Path: /repo-folder +- Path: repo-folder - Port (optional, default is 21): Password (min. 10 chars): (user enters repo password) @@ -68,7 +74,7 @@ This sets up a new repository on the given remote storage and initializes the local folder. You can now use `sy connect` to connect to this repository from other clients. -**2. Add files and synchronize** +**3. Add files and synchronize** To let Syncany do everything automatically, simple use the `sy watch` command. This command will synchronize your local files. @@ -84,7 +90,7 @@ $ sy up $ sy down ``` -**3. Connect other clients** +**4. Connect other clients** To connect new clients to an existing repository, use the `sy connect` command. This will set up your local folder to sync with the chosen remote repository. @@ -115,7 +121,7 @@ Break some hashes for us and [donate some Bitcoins](https://blockchain.info/addr Licensing, website and contact ------------------------------ -Syncany is licensed under the GPLv2 open source license. It is mainly developed by [Philipp C. Heckel](http://blog.philippheckel.com/). We are always looking for people to join or help out. Feel free to contact us: +Syncany is licensed under the GPLv2 open source license. It is actively developed by [Philipp C. Heckel](http://blog.philippheckel.com/) and [many others](https://github.com/binwiederhier/syncany/graphs/contributors). We are always looking for people to join or help out. Feel free to contact us: - [Syncany website](https://www.syncany.org/), still with screenshots of the old interface - [Syncany wiki page](https://github.com/binwiederhier/syncany/wiki), **most important resource, and always updated** diff --git a/syncany-cli/build.gradle b/syncany-cli/build.gradle index 01e6589d..212c7f73 100644 --- a/syncany-cli/build.gradle +++ b/syncany-cli/build.gradle @@ -25,14 +25,23 @@ mainClassName = "org.syncany.Syncany" startScripts { defaultJvmOpts = [ "-Xmx1024m", "-Dfile.encoding=utf-8" ] - classpath = files('$APP_HOME/lib/*') doLast { def winFile = file getWindowsScript() def unixFile = file getUnixScript() - winFile.text = winFile.text.replaceAll("(set CLASSPATH=.+)", '$1;%AppData%\\\\syncany\\\\plugins\\\\*') - unixFile.text = unixFile.text.replaceAll("(CLASSPATH=\\\$APP_HOME.+)", '$1:~/.config/syncany/plugins/*') + winFile.text = winFile.text.replaceAll("(set CLASSPATH=.+)", 'set CLASSPATH=%APP_HOME%\\\\lib\\\\*;%AppData%\\\\Syncany\\\\plugins\\\\*') + unixFile.text = unixFile.text.replaceAll("(CLASSPATH=\\\$APP_HOME.+)", 'CLASSPATH=\\\$APP_HOME/lib/*:~/.config/syncany/plugins/*') + + // Post Java process commands: Delayed plugin JAR file deletion (Windows only) + String winPurgeFileDeletionCommands = "@rem Delete plugin JARs\r\n" + winPurgeFileDeletionCommands += "SET PURGEFILE=%AppData%\\\\Syncany\\\\purgefile\r\n"; + winPurgeFileDeletionCommands += "if exist %PURGEFILE% (\r\n"; + winPurgeFileDeletionCommands += " @for /f %%b in (%PURGEFILE%) do del /q \"%%b\" 2>NUL\r\n"; + winPurgeFileDeletionCommands += " del /q %PURGEFILE% 2>NUL\r\n"; + winPurgeFileDeletionCommands += ")\r\n\r\n"; + + winFile.text = winFile.text.replaceAll("(:end)", "${winPurgeFileDeletionCommands}:end") } } diff --git a/syncany-cli/src/main/java/org/syncany/cli/AbstractInitCommand.java b/syncany-cli/src/main/java/org/syncany/cli/AbstractInitCommand.java index 21160f86..63311172 100644 --- a/syncany-cli/src/main/java/org/syncany/cli/AbstractInitCommand.java +++ b/syncany-cli/src/main/java/org/syncany/cli/AbstractInitCommand.java @@ -17,13 +17,13 @@ */ package org.syncany.cli; -import java.net.InetAddress; +import java.math.BigInteger; import java.net.UnknownHostException; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Random; import joptsimple.OptionSet; import joptsimple.OptionSpec; @@ -33,11 +33,11 @@ import org.syncany.connection.plugins.Connection; import org.syncany.connection.plugins.Plugin; import org.syncany.connection.plugins.PluginOptionSpec; -import org.syncany.connection.plugins.StorageTestResult; import org.syncany.connection.plugins.PluginOptionSpec.OptionValidationResult; import org.syncany.connection.plugins.PluginOptionSpecs; import org.syncany.connection.plugins.Plugins; import org.syncany.connection.plugins.StorageException; +import org.syncany.connection.plugins.StorageTestResult; import org.syncany.operations.init.GenlinkOperationResult; import org.syncany.util.StringUtil; import org.syncany.util.StringUtil.StringJoinListener; @@ -54,7 +54,7 @@ protected ConfigTO createConfigTO(ConnectionTO connectionTO) throws Exception { ConfigTO configTO = new ConfigTO(); configTO.setDisplayName(getDefaultDisplayName()); - configTO.setMachineName(getDefaultMachineName()); + configTO.setMachineName(getRandomMachineName()); configTO.setMasterKey(null); configTO.setConnectionTO(connectionTO); // can be null @@ -289,9 +289,9 @@ public String getString(Plugin plugin) { return plugin; } - protected String getDefaultMachineName() throws UnknownHostException { - return new String(InetAddress.getLocalHost().getHostName() + System.getProperty("user.name") + Math.abs(new Random().nextInt())).replaceAll( - "[^a-zA-Z0-9]", ""); + protected String getRandomMachineName() { + String randomStr = new BigInteger(128, new SecureRandom()).toString(32); + return (randomStr.length() > 16) ? randomStr.substring(0, 16) : randomStr; } protected String getDefaultDisplayName() throws UnknownHostException { diff --git a/syncany-cli/src/main/java/org/syncany/cli/CommandLineClient.java b/syncany-cli/src/main/java/org/syncany/cli/CommandLineClient.java index fb2dd991..059920af 100644 --- a/syncany-cli/src/main/java/org/syncany/cli/CommandLineClient.java +++ b/syncany-cli/src/main/java/org/syncany/cli/CommandLineClient.java @@ -26,9 +26,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.io.PrintStream; -import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Date; import java.util.List; import java.util.logging.ConsoleHandler; import java.util.logging.FileHandler; @@ -38,7 +36,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import joptsimple.OptionException; import joptsimple.OptionParser; import joptsimple.OptionSet; import joptsimple.OptionSpec; @@ -64,6 +61,10 @@ public class CommandLineClient extends Client { private static final Logger logger = Logger.getLogger(CommandLineClient.class.getSimpleName()); + private static final String LOG_FILE_PATTERN = "syncany.log"; + private static final int LOG_FILE_COUNT = 4; + private static final int LOG_FILE_LIMIT = 25000000; // 25 MB + private static final Pattern HELP_TEXT_RESOURCE_PATTERN = Pattern.compile("\\%RESOURCE:([^%]+)\\%"); private static final String HELP_TEXT_HELP_SKEL_RESOURCE = "/help/help.skel"; private static final String HELP_TEXT_USAGE_SKEL_RESOURCE = "/help/usage.skel"; @@ -110,14 +111,14 @@ public int start() throws Exception { // Evaluate options // WARNING: Do not re-order unless you know what you are doing! - //initHelpOption(options, optionHelp, options.nonOptionArguments()); initConfigOption(options, optionLocalDir); initLogOption(options, optionLog, optionLogLevel, optionLogPrint, optionDebug); // Run! return runCommand(options, optionHelp, options.nonOptionArguments()); } - catch (OptionException e) { + catch (Exception e) { + logger.log(Level.SEVERE, "Exception while initializing or running command.", e); return showErrorAndExit(e.getMessage()); } } @@ -165,11 +166,11 @@ private void initLogHandlers(OptionSet options, OptionSpec optionLog, Op } } else if (config != null && config.getLogDir().exists()) { - logFilePattern = config.getLogDir()+File.separator+new SimpleDateFormat("yyMMdd").format(new Date())+".log"; + logFilePattern = config.getLogDir() + File.separator + LOG_FILE_PATTERN; } if (logFilePattern != null) { - Handler fileLogHandler = new FileHandler(logFilePattern, true); + Handler fileLogHandler = new FileHandler(logFilePattern, LOG_FILE_LIMIT, LOG_FILE_COUNT, true); fileLogHandler.setFormatter(new LogFormatter()); Logging.addGlobalHandler(fileLogHandler); @@ -221,12 +222,12 @@ private int runCommand(OptionSet options, OptionSpec optionHelp, List n Command command = CommandFactory.getInstance(commandName); if (command == null) { - showErrorAndExit("Given command is unknown: "+commandName); + return showErrorAndExit("Given command is unknown: "+commandName); } // Potentially show help if (options.has(optionHelp)) { - showCommandHelpAndExit(commandName); + return showCommandHelpAndExit(commandName); } // Init command @@ -237,12 +238,12 @@ private int runCommand(OptionSet options, OptionSpec optionHelp, List n // Pre-init operations if (command.getRequiredCommandScope() == INITIALIZED_LOCALDIR) { if (config == null) { - showErrorAndExit("No repository found in path. Use 'init' command to create one."); + return showErrorAndExit("No repository found in path, or configured plugin not installed. Use 'sy init' to create one."); } } else if (command.getRequiredCommandScope() == UNINITIALIZED_LOCALDIR) { if (config != null) { - showErrorAndExit("Repository found in path. Command can only be used outside a repository."); + return showErrorAndExit("Repository found in path. Command can only be used outside a repository."); } } @@ -253,10 +254,8 @@ else if (command.getRequiredCommandScope() == UNINITIALIZED_LOCALDIR) { } catch (Exception e) { logger.log(Level.SEVERE, "Command "+ commandName+" FAILED. ", e); - showErrorAndExit(e.getMessage()); + return showErrorAndExit(e.getMessage()); } - - return -1; // Never reached! } private void showUsageAndExit() throws IOException { @@ -267,12 +266,12 @@ private void showHelpAndExit() throws IOException { printHelpTextAndExit(HELP_TEXT_HELP_SKEL_RESOURCE); } - private void showCommandHelpAndExit(String commandName) throws IOException { + private int showCommandHelpAndExit(String commandName) throws IOException { String helpTextResource = HELP_TEXT_CMD_SKEL_RESOURCE.replace(HELP_VAR_CMD, commandName); - printHelpTextAndExit(helpTextResource); + return printHelpTextAndExit(helpTextResource); } - private void printHelpTextAndExit(String helpTextResource) throws IOException { + private int printHelpTextAndExit(String helpTextResource) throws IOException { InputStream helpTextInputStream = CommandLineClient.class.getResourceAsStream(helpTextResource); if (helpTextInputStream == null) { @@ -286,6 +285,8 @@ private void printHelpTextAndExit(String helpTextResource) throws IOException { out.close(); System.exit(0); + + return -1; // Never reached } private String replaceVariables(String line) throws IOException { @@ -355,7 +356,7 @@ private int showErrorAndExit(String errorMessage) { out.close(); System.exit(0); - return 0; + return -1; // Never reached } } diff --git a/syncany-cli/src/main/resources/help/cmd/help.connect.skel b/syncany-cli/src/main/resources/help/cmd/help.connect.skel new file mode 100644 index 00000000..bbaa07ef --- /dev/null +++ b/syncany-cli/src/main/resources/help/cmd/help.connect.skel @@ -0,0 +1,44 @@ +%RESOURCE:/help/copyright.skel% + +SYNOPSIS + sy connect + + sy connect [-P | --plugin=] [-o | --plugin-option=] + [-I | --no-interaction] + +DESCRIPTION + This command connects to an existing remote repository and initializes + the local directory. + + The command can be called as follows: + + 1. Using a syncany://-link generated by either 'init' or 'genlink', + the command connects to the repository given in the link. If the link + is encrypted, the link/repo password must be entered. + + 2. If no link is given, the command acts like 'init', i.e. it queries the + user for storage plugin and connection details of the repository to + connect to. + + Once the repository is connected, the initialized local folder can be synced + with the newly created repository. The commands 'up', 'down', 'watch', etc. + can be used. Other clients can then be connected using the 'connect' command. + +OPTIONS + -P, --plugin= + Selects a plugin to use for the repository. Local files will be synced via + the storage specified by this plugin. Any of the following available + plugins can be used: %PLUGINS% + + -o, --plugin-option= (multiple options possible) + Sets a plugin-specific setting in the form of a key/value pair. Each + plugin defines different mandatory and optional settings that must/can + either be specified by this option, or interactively. All mandatory and + optional settings can be listed using the 'plugin' command. + + -I, --no-interaction + Runs the command in a non-interactive mode. The user will not be queried + for any input. The command will fail if not all mandatory options are + given on the command line. This option can be used to automate repository + creation. + \ No newline at end of file diff --git a/syncany-cli/src/main/resources/help/help.skel b/syncany-cli/src/main/resources/help/help.skel index e18869ca..853b9c99 100644 --- a/syncany-cli/src/main/resources/help/help.skel +++ b/syncany-cli/src/main/resources/help/help.skel @@ -22,6 +22,7 @@ DESCRIPTION restore Restore the given file paths from the remote repository. genlink Create a syncany://-link from an existing local folder. log Print parts of the local database to STDOUT. + plugin List, install and remove storage backend plugins. Short command descriptions and options can be found below. Detailed explanations can be queried with `sy --help`. diff --git a/syncany-cli/src/test/java/org/syncany/tests/cli/StatusCommandTest.java b/syncany-cli/src/test/java/org/syncany/tests/cli/StatusCommandTest.java index 812b0c78..5c27df10 100644 --- a/syncany-cli/src/test/java/org/syncany/tests/cli/StatusCommandTest.java +++ b/syncany-cli/src/test/java/org/syncany/tests/cli/StatusCommandTest.java @@ -76,7 +76,7 @@ public void testStatusCommandWithLogFile() throws Exception { })); // Test - assertTrue("Log file should exist.", tempLogFile.exists()); + assertTrue("Log file should exist.", new File(tempLogFile.getAbsolutePath() + ".0").exists()); assertEquals(2, cliOut.length); assertEquals("? somefolder1", cliOut[0]); assertEquals("? somefolder2", cliOut[1]); diff --git a/syncany-lib/build.gradle b/syncany-lib/build.gradle index 4b13a89f..642b32c0 100644 --- a/syncany-lib/build.gradle +++ b/syncany-lib/build.gradle @@ -19,8 +19,8 @@ dependencies { compile "commons-codec:commons-codec:1.8" compile "org.reflections:reflections:0.9.8" compile "org.slf4j:slf4j-api:1.6.0" // for reflections - compile "org.hsqldb:hsqldb:2.3.1" + compile "com.github.zafarkhaja:java-semver:0.7.2" testCompile project(path: ':syncany-util', configuration: 'tests') testCompile "junit:junit:4.9" diff --git a/syncany-lib/src/main/java/org/syncany/Client.java b/syncany-lib/src/main/java/org/syncany/Client.java index 7b1a01cf..693ce477 100644 --- a/syncany-lib/src/main/java/org/syncany/Client.java +++ b/syncany-lib/src/main/java/org/syncany/Client.java @@ -17,12 +17,12 @@ */ package org.syncany; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.Properties; import org.syncany.config.Config; +import org.syncany.config.UserConfig; import org.syncany.connection.plugins.StorageException; import org.syncany.crypto.CipherException; import org.syncany.operations.CleanupOperation; @@ -66,7 +66,6 @@ import org.syncany.operations.watch.WatchOperation; import org.syncany.operations.watch.WatchOperation.WatchOperationListener; import org.syncany.operations.watch.WatchOperation.WatchOperationOptions; -import org.syncany.util.EnvironmentUtil; /** * The client class is a convenience class to call the application's {@link Operation}s @@ -83,15 +82,13 @@ public class Client { private static final String APPLICATION_PROPERTIES_VERSION_KEY = "applicationVersion"; private static final String APPLICATION_PROPERTIES_REVISION_KEY = "applicationRevision"; - private static Properties applicationProperties; - private static File userAppDir; - private static File userPluginsDir; - + private static Properties applicationProperties; + protected Config config; static { + initUserConfig(); initApplicationProperties(); - initUserAppDirs(); } public void setConfig(Config config) { @@ -203,24 +200,9 @@ public static String getApplicationVersion() { public static String getApplicationRevision() { return applicationProperties.getProperty(APPLICATION_PROPERTIES_REVISION_KEY); } - - public static File getUserAppDir() { - return userAppDir; - } - - public static File getUserPluginDir() { - return userPluginsDir; - } - private static void initUserAppDirs() { - if (EnvironmentUtil.isWindows()) { - userAppDir = new File(System.getProperty("user.home") + "\\Syncany"); - } - else { - userAppDir = new File(System.getProperty("user.home") + "/.config/syncany"); - } - - userPluginsDir = new File(userAppDir, "plugins"); + private static void initUserConfig() { + UserConfig.init(); } private static void initApplicationProperties() { @@ -233,5 +215,5 @@ private static void initApplicationProperties() { catch (Exception e) { throw new RuntimeException("Cannot load application properties.", e); } - } + } } diff --git a/syncany-lib/src/main/java/org/syncany/chunk/ZipMultiChunk.java b/syncany-lib/src/main/java/org/syncany/chunk/ZipMultiChunk.java index 4d8a6662..73c698cd 100644 --- a/syncany-lib/src/main/java/org/syncany/chunk/ZipMultiChunk.java +++ b/syncany-lib/src/main/java/org/syncany/chunk/ZipMultiChunk.java @@ -111,9 +111,14 @@ public void close() throws IOException { if (zipOut != null) { zipOut.close(); } - else { + + if (zipIn != null) { zipIn.close(); } - } + + if (zipFile != null) { + zipFile.close(); + } + } } diff --git a/syncany-lib/src/main/java/org/syncany/config/ConfigHelper.java b/syncany-lib/src/main/java/org/syncany/config/ConfigHelper.java index c3bc9770..c18d8a3f 100644 --- a/syncany-lib/src/main/java/org/syncany/config/ConfigHelper.java +++ b/syncany-lib/src/main/java/org/syncany/config/ConfigHelper.java @@ -27,6 +27,8 @@ import org.syncany.config.Config.ConfigException; import org.syncany.config.to.ConfigTO; import org.syncany.config.to.RepoTO; +import org.syncany.connection.plugins.Plugin; +import org.syncany.connection.plugins.Plugins; import org.syncany.crypto.CipherUtil; import org.syncany.crypto.SaltedSecretKey; @@ -44,7 +46,7 @@ public static Config loadConfig(File localDir) throws ConfigException { throw new ConfigException("Argument localDir cannot be null."); } - File appDir = new File(localDir+"/"+Config.DIR_APPLICATION); + File appDir = new File(localDir, Config.DIR_APPLICATION); if (appDir.exists()) { logger.log(Level.INFO, "Loading config from {0} ...", localDir); @@ -52,7 +54,17 @@ public static Config loadConfig(File localDir) throws ConfigException { ConfigTO configTO = ConfigHelper.loadConfigTO(localDir); RepoTO repoTO = ConfigHelper.loadRepoTO(localDir, configTO); - return new Config(localDir, configTO, repoTO); + String pluginId = (configTO.getConnectionTO() != null) ? configTO.getConnectionTO().getType() : null; + Plugin plugin = Plugins.get(pluginId); + + if (plugin == null) { + logger.log(Level.WARNING, "Not loading config! Plugin with id '{0}' does not exist.", pluginId); + return null; + } + else { + logger.log(Level.INFO, "Initializing Config instance ..."); + return new Config(localDir, configTO, repoTO); + } } else { logger.log(Level.INFO, "Not loading config, app dir does not exist: {0}", appDir); diff --git a/syncany-lib/src/main/java/org/syncany/config/IgnoredFiles.java b/syncany-lib/src/main/java/org/syncany/config/IgnoredFiles.java index afa89b6a..8010fc86 100644 --- a/syncany-lib/src/main/java/org/syncany/config/IgnoredFiles.java +++ b/syncany-lib/src/main/java/org/syncany/config/IgnoredFiles.java @@ -80,7 +80,13 @@ public void loadPatterns() { ignorePatterns.add(ignorePattern.substring(6)); } else { - ignorePaths.add(ignorePattern); + if (ignorePattern.contains("*") || ignorePattern.contains("?")) { + // wildcards handling, converting them to regexps + ignorePatterns.add(convertWildcardsToRegexp(ignorePattern)); + } + else { + ignorePaths.add(ignorePattern); + } } } } @@ -96,4 +102,33 @@ public void loadPatterns() { ignorePaths = new HashSet(); } } + + private static String convertWildcardsToRegexp(String in) { + StringBuilder out = new StringBuilder("^"); + + for (int i = 0; i < in.length(); ++i) { + char c = in.charAt(i); + + switch (c) { + case '*': + out.append(".*"); + break; + case '?': + out.append("."); + break; + case '.': case '$': case '^': + case '{': case '}': case '[': case ']': case '(': case ')': + case '|': case '+': case '\\': + out.append("\\"); + out.append(c); + break; + default: + out.append(c); + } + } + + out.append('$'); + + return out.toString(); + } } diff --git a/syncany-lib/src/main/java/org/syncany/config/UserConfig.java b/syncany-lib/src/main/java/org/syncany/config/UserConfig.java new file mode 100644 index 00000000..1b4801a9 --- /dev/null +++ b/syncany-lib/src/main/java/org/syncany/config/UserConfig.java @@ -0,0 +1,109 @@ +/* + * Syncany, www.syncany.org + * Copyright (C) 2011-2014 Philipp C. Heckel + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package org.syncany.config; + +import java.io.File; +import java.util.Map; + +import org.syncany.config.Config.ConfigException; +import org.syncany.config.to.UserConfigTO; +import org.syncany.util.EnvironmentUtil; + +/** + * Represents the configuration parameters and application user directory + * of the currently logged in user, including system properties that will be + * set with every application start. + * + * @author Philipp C. Heckel + */ +public class UserConfig { + private static final File USER_APP_DIR_WINDOWS = new File(System.getenv("APPDATA") + "\\Syncany"); + private static final File USER_APP_DIR_UNIX_LIKE = new File(System.getProperty("user.home") + "/.config/syncany"); + private static final String USER_PLUGINS_DIR = "plugins"; + private static final String USER_CONFIG_FILE = "userconfig.xml"; + + private static File userAppDir; + private static File userPluginsDir; + + static { + init(); + } + + public static void init() { + if (userAppDir == null) { + initUserAppDirs(); + initUserConfig(); + } + } + + public static File getUserAppDir() { + return userAppDir; + } + + public static File getUserPluginDir() { + return userPluginsDir; + } + + private static void initUserAppDirs() { + userAppDir = (EnvironmentUtil.isWindows()) ? USER_APP_DIR_WINDOWS : USER_APP_DIR_UNIX_LIKE; + userAppDir.mkdirs(); + + userPluginsDir = new File(userAppDir, USER_PLUGINS_DIR); + userPluginsDir.mkdirs(); + } + + private static void initUserConfig() { + File userConfigFile = new File(userAppDir, USER_CONFIG_FILE); + + if (userConfigFile.exists()) { + loadAndInitUserConfigFile(userConfigFile); + } + else { + writeExampleUserConfigFile(userConfigFile); + } + } + + private static void loadAndInitUserConfigFile(File userConfigFile) { + try { + UserConfigTO userConfigTO = UserConfigTO.load(userConfigFile); + + for (Map.Entry systemProperty : userConfigTO.getSystemProperties().entrySet()) { + System.setProperty(systemProperty.getKey(), systemProperty.getValue()); + } + } + catch (ConfigException e) { + System.err.println("ERROR: " + e.getMessage()); + System.err.println(" Ignoring user config file!"); + System.err.println(); + } + } + + private static void writeExampleUserConfigFile(File userConfigFile) { + UserConfigTO userConfigTO = new UserConfigTO(); + + userConfigTO.getSystemProperties().put("example.property", "This is a demo property. You can delete it."); + userConfigTO.getSystemProperties().put("syncany.rocks", "Yes, it does!"); + + try { + UserConfigTO.save(userConfigTO, userConfigFile); + } + catch (Exception e) { + // Don't care! + } + } +} diff --git a/syncany-lib/src/main/java/org/syncany/config/to/UserConfigTO.java b/syncany-lib/src/main/java/org/syncany/config/to/UserConfigTO.java new file mode 100644 index 00000000..3bfe1f8e --- /dev/null +++ b/syncany-lib/src/main/java/org/syncany/config/to/UserConfigTO.java @@ -0,0 +1,61 @@ +/* + * Syncany, www.syncany.org + * Copyright (C) 2011-2014 Philipp C. Heckel + * + * This program 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. + * + * This program 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 this program. If not, see . + */ +package org.syncany.config.to; + +import java.io.File; +import java.util.Map; +import java.util.TreeMap; + +import org.simpleframework.xml.ElementMap; +import org.simpleframework.xml.Namespace; +import org.simpleframework.xml.Root; +import org.simpleframework.xml.core.Persister; +import org.syncany.config.Config.ConfigException; + +@Root(name="userConfig") +@Namespace(reference="http://syncany.org/userconfig/1") +public class UserConfigTO { + @ElementMap(name="systemProperties", entry="property", key="name", required=false, attribute=true) + private TreeMap systemProperties; + + public UserConfigTO() { + this.systemProperties = new TreeMap(); + } + + public Map getSystemProperties() { + return systemProperties; + } + + public static UserConfigTO load(File file) throws ConfigException { + try { + return new Persister().read(UserConfigTO.class, file); + } + catch (Exception e) { + throw new ConfigException("User config file cannot be read or is invalid: " + file, e); + } + } + + public static void save(UserConfigTO userConfigTO, File file) throws ConfigException { + try { + new Persister().write(userConfigTO, file); + } + catch (Exception e) { + throw new ConfigException("Cannot write user config to file " + file, e); + } + } +} diff --git a/syncany-lib/src/main/java/org/syncany/connection/plugins/AbstractTransferManager.java b/syncany-lib/src/main/java/org/syncany/connection/plugins/AbstractTransferManager.java index 545e758b..e3b97b56 100644 --- a/syncany-lib/src/main/java/org/syncany/connection/plugins/AbstractTransferManager.java +++ b/syncany-lib/src/main/java/org/syncany/connection/plugins/AbstractTransferManager.java @@ -57,8 +57,16 @@ public StorageTestResult test(boolean testCreateTarget) { result.setTargetCanWrite(testTargetCanWrite()); result.setRepoFileExists(testRepoFileExists()); - if (!result.isTargetExists() && testCreateTarget) { - result.setTargetCanCreate(testTargetCanCreate()); + if (result.isTargetExists()) { + result.setTargetCanCreate(true); + } + else { + if (testCreateTarget) { + result.setTargetCanCreate(testTargetCanCreate()); + } + else { + result.setTargetCanCreate(false); + } } result.setTargetCanConnect(true); diff --git a/syncany-lib/src/main/java/org/syncany/connection/plugins/StorageTestResult.java b/syncany-lib/src/main/java/org/syncany/connection/plugins/StorageTestResult.java index 53ed2e3f..5635ce93 100644 --- a/syncany-lib/src/main/java/org/syncany/connection/plugins/StorageTestResult.java +++ b/syncany-lib/src/main/java/org/syncany/connection/plugins/StorageTestResult.java @@ -18,8 +18,21 @@ package org.syncany.connection.plugins; /** - * @author pheckel - * + * Represents the return structure of the tests performed by by {@link TransferManager#test(boolean)} + * method. + * + *

The result is determined by the following methods: + * + *

    + *
  • {@link TransferManager#testTargetExists()}: Tests whether the target exists.
  • + *
  • {@link TransferManager#testTargetCanWrite()}: Tests whether the target is writable.
  • + *
  • {@link TransferManager#testTargetCanCreate()}: Tests whether the target can be created if it does not + * exist already. This is only called if testCreateTarget is set.
  • + *
  • {@link TransferManager#testRepoFileExists()}: Tests whether the repo file exists.
  • + *
+ * + * @see TransferManager#test(boolean) + * @author Philipp Heckel */ public class StorageTestResult { private boolean targetExists; diff --git a/syncany-lib/src/main/java/org/syncany/connection/plugins/TransferManager.java b/syncany-lib/src/main/java/org/syncany/connection/plugins/TransferManager.java index c84ee666..b5d79a59 100644 --- a/syncany-lib/src/main/java/org/syncany/connection/plugins/TransferManager.java +++ b/syncany-lib/src/main/java/org/syncany/connection/plugins/TransferManager.java @@ -139,51 +139,75 @@ public interface TransferManager { /** * Tests whether the repository parameters are valid. In particular, the method tests - * whether a repository exists or, if not, whether it can be created. + * whether a target (folder, bucket, etc.) exists or, if not, whether it can be created. + * It furthermore tests whether a repository at the target already exists by checking if the + * {@link RepoRemoteFile} exists. + * + *

The relevant result is determined by the following methods: * *

    - *
  • {@link StorageTestResult#NO_CONNECTION}: If the Internet connection is broken, or the - * socket broke.
  • - *
  • {@link StorageTestResult#NO_REPO}: No repository exists on the remote location, i.e. - * not even the folder/path exists.
  • - *
  • {@link StorageTestResult#NO_REPO_CANNOT_CREATE}: No repository exists and it cannot be - * created, because write access is missing.
  • - *
  • {@link StorageTestResult#REPO_EXISTS}: The repository exists and is valid.
  • - *
  • {@link StorageTestResult#REPO_EXISTS_BUT_INVALID}: The repository path/folder exists, but - * and is not valid.
  • + *
  • {@link #testTargetExists()}: Tests whether the target exists.
  • + *
  • {@link #testTargetCanWrite()}: Tests whether the target is writable.
  • + *
  • {@link #testTargetCanCreate()}: Tests whether the target can be created if it does not + * exist already. This is only called if testCreateTarget is set.
  • + *
  • {@link #testRepoFileExists()}: Tests whether the repo file exists.
  • *
* * @return Returns the result of testing the repository. + * @param testCreateTarget If true, the test will test if the target can be created in case + * it does not exist. If false, this test will be skipped. * @see {@link StorageTestResult} */ public StorageTestResult test(boolean testCreateTarget); /** - * Tests whether the repository path/folder is writable by the application. This method is - * called by the {@link #test()} method (only during repository initialization (or initial - * connection). + * Tests whether the target path/folder exists. This might be done by listing the parent path/folder + * or by retrieving metadata about the target. The method returns true if the target exists, + * false otherwise. + * + *

This method is called by the {@link #test(boolean)} method (only during repository initialization + * or initial connection). * - * @return Returns true if the repository can be written to, false otherwise + * @return Returns true if the target exists, false otherwise */ - public boolean testTargetCanWrite() throws StorageException; + public boolean testTargetExists(); + + /** + * Tests whether the target path/folder is writable by the application. This method may either + * check the write permissions of the target or actually write a test file to check write access. If the + * target does not exist, false is returned. If the target exists and is writable, true + * is returned. + * + *

This method is called by the {@link #test(boolean)} method (only during repository initialization + * or initial connection). + * + * @return Returns true if the target can be written to, false otherwise + */ + public boolean testTargetCanWrite(); /** - * Tests whether the repository path/folder is accessible and exists. This method is - * called by the {@link #test()} method (only during repository initialization (or initial - * connection). + * Tests whether the target path/folder can be created (if it does not exist already). This method + * may either check the permissions of the parent path/folder or actually create and delete the target to + * determine create permissions. + * + *

If the target already exists, the method returns true. If it does not, but it can be created + * (according to tests of this method), it also returns true. In all other cases, false is returned. + * + *

This method is called by the {@link #test(boolean)} method, but only if the testCreateTarget flag + * is set to true! * - * @return Returns true if the repository can be written to, false otherwise + * @return Returns true if the target can be created or already exists, false otherwise */ - public boolean testTargetExists() throws StorageException; - - public boolean testTargetCanCreate() throws StorageException; + public boolean testTargetCanCreate(); /** - * Tests whether the repository path/folder is accessible and the repository file - * exists (see {@link RepoRemoteFile}). This method is called by the {@link #test()} method + * Tests whether the repository file exists (see {@link RepoRemoteFile}). This method is called by the {@link #test()} method * (only during repository initialization (or initial connection). * + *

This method is called by the {@link #test(boolean)} method (only during repository initialization + * or initial connection). + * * @return Returns true if the repository is valid, false otherwise */ - public boolean testRepoFileExists() throws StorageException; + public boolean testRepoFileExists(); } diff --git a/syncany-lib/src/main/java/org/syncany/connection/plugins/local/LocalTransferManager.java b/syncany-lib/src/main/java/org/syncany/connection/plugins/local/LocalTransferManager.java index ca744a80..7b5e1e64 100644 --- a/syncany-lib/src/main/java/org/syncany/connection/plugins/local/LocalTransferManager.java +++ b/syncany-lib/src/main/java/org/syncany/connection/plugins/local/LocalTransferManager.java @@ -253,7 +253,7 @@ public boolean testTargetCanWrite() { } @Override - public boolean testTargetExists() throws StorageException { + public boolean testTargetExists() { if (repoPath.exists()) { logger.log(Level.INFO, "testTargetExists: Target exists."); return true; @@ -265,21 +265,27 @@ public boolean testTargetExists() throws StorageException { } @Override - public boolean testRepoFileExists() throws StorageException { - File repoFile = getRemoteFile(new RepoRemoteFile()); - - if (repoFile.exists()) { - logger.log(Level.INFO, "testRepoFileExists: Repo file exists, list(syncany) returned one result."); - return true; + public boolean testRepoFileExists() { + try { + File repoFile = getRemoteFile(new RepoRemoteFile()); + + if (repoFile.exists()) { + logger.log(Level.INFO, "testRepoFileExists: Repo file exists, list(syncany) returned one result."); + return true; + } + else { + logger.log(Level.INFO, "testRepoFileExists: Repo file DOES NOT exist."); + return false; + } } - else { - logger.log(Level.INFO, "testRepoFileExists: Repo file DOES NOT exist."); - return false; + catch (Exception e) { + logger.log(Level.INFO, "testRepoFileExists: Repo file DOES NOT exist. Exception occurred.", e); + return false; } } @Override - public boolean testTargetCanCreate() throws StorageException { + public boolean testTargetCanCreate() { if (repoPath.getParentFile().canWrite()) { logger.log(Level.INFO, "testTargetCanCreate: Can create target."); return true; diff --git a/syncany-lib/src/main/java/org/syncany/operations/down/actions/FileCreatingFileSystemAction.java b/syncany-lib/src/main/java/org/syncany/operations/down/actions/FileCreatingFileSystemAction.java index e6115914..01c503e2 100644 --- a/syncany-lib/src/main/java/org/syncany/operations/down/actions/FileCreatingFileSystemAction.java +++ b/syncany-lib/src/main/java/org/syncany/operations/down/actions/FileCreatingFileSystemAction.java @@ -124,6 +124,9 @@ private File assembleFileToCache(FileVersion reconstructedFileVersion) throws Ex InputStream chunkInputStream = multiChunk.getChunkInputStream(chunkChecksum.getRaw()); FileUtil.appendToOutputStream(chunkInputStream, reconstructedFileOutputStream); + + chunkInputStream.close(); + multiChunk.close(); } } diff --git a/syncany-lib/src/main/java/org/syncany/operations/plugin/PluginOperation.java b/syncany-lib/src/main/java/org/syncany/operations/plugin/PluginOperation.java index ea1c4dfd..0c8ae459 100644 --- a/syncany-lib/src/main/java/org/syncany/operations/plugin/PluginOperation.java +++ b/syncany-lib/src/main/java/org/syncany/operations/plugin/PluginOperation.java @@ -18,12 +18,15 @@ package org.syncany.operations.plugin; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; +import java.io.PrintWriter; import java.net.URL; import java.net.URLConnection; import java.util.ArrayList; @@ -39,6 +42,7 @@ import org.simpleframework.xml.core.Persister; import org.syncany.Client; import org.syncany.config.Config; +import org.syncany.config.UserConfig; import org.syncany.connection.plugins.Plugin; import org.syncany.connection.plugins.Plugins; import org.syncany.crypto.CipherUtil; @@ -46,9 +50,12 @@ import org.syncany.operations.plugin.PluginOperationOptions.PluginAction; import org.syncany.operations.plugin.PluginOperationOptions.PluginListMode; import org.syncany.operations.plugin.PluginOperationResult.PluginResultCode; +import org.syncany.util.EnvironmentUtil; import org.syncany.util.FileUtil; import org.syncany.util.StringUtil; +import com.github.zafarkhaja.semver.Version; + /** * The plugin operation installs, removes and lists storage {@link Plugin}s. * @@ -76,7 +83,8 @@ public class PluginOperation extends Operation { private static final Logger logger = Logger.getLogger(PluginOperation.class.getSimpleName()); - private static final String PLUGIN_LIST_URL = "https://api.syncany.org/v1/plugins/list?appVersion=%s&snapshots=%s&pluginId=%s"; + private static final String PLUGIN_LIST_URL = "https://api.syncany.org/v1/plugins/list?appVersion=%s&snapshots=%s&pluginId=%s"; + private static final String PURGEFILE_FILENAME = "purgefile"; private PluginOperationOptions options; private PluginOperationResult result; @@ -118,7 +126,7 @@ private PluginOperationResult executeRemove() throws Exception { String pluginClassLocationStr = pluginClassLocation.toString(); logger.log(Level.INFO, "Plugin class is at " + pluginClassLocation); - File globalUserPluginDir = Client.getUserPluginDir(); + File globalUserPluginDir = UserConfig.getUserPluginDir(); int indexStartAfterSchema = "jar:file:".length(); int indexEndAtExclamationPoint = pluginClassLocationStr.indexOf("!"); @@ -133,6 +141,18 @@ private PluginOperationResult executeRemove() throws Exception { logger.log(Level.INFO, "Uninstalling plugin from file " + pluginJarFile); pluginJarFile.delete(); + // JAR files are locked on Windows, adding JAR filename to a list for delayed deletion (by batch file) + if (EnvironmentUtil.isWindows()) { + File purgefilePath = new File(UserConfig.getUserAppDir(), PURGEFILE_FILENAME); + + try (PrintWriter out = new PrintWriter(new BufferedWriter(new FileWriter(purgefilePath, true)))) { + out.println(pluginJarFile.getAbsolutePath()); + } + catch (IOException e) { + logger.log(Level.SEVERE, "Unable to append to purgefile " + purgefilePath, e); + } + } + result.setSourcePluginPath(pluginJarFile.getAbsolutePath()); result.setAffectedPluginInfo(pluginInfo); result.setResultCode(PluginResultCode.OK); @@ -169,6 +189,8 @@ private PluginOperationResult executeInstallFromApiHost(String pluginId) throws throw new Exception("Plugin with ID '" + pluginId + "' not found"); } + checkPluginCompatibility(pluginInfo); + File tempPluginJarFile = downloadPluginJar(pluginInfo.getDownloadUrl()); String expectedChecksum = pluginInfo.getSha256sum(); String actualChecksum = calculateChecksum(tempPluginJarFile); @@ -176,6 +198,8 @@ private PluginOperationResult executeInstallFromApiHost(String pluginId) throws if (expectedChecksum == null || !expectedChecksum.equals(actualChecksum)) { throw new Exception("Checksum mismatch. Expected: " + expectedChecksum + ", but was: " + actualChecksum); } + + logger.log(Level.INFO, "Plugin JAR checksum verified: " + actualChecksum); File targetPluginJarFile = installPlugin(tempPluginJarFile, pluginInfo); @@ -187,6 +211,20 @@ private PluginOperationResult executeInstallFromApiHost(String pluginId) throws return result; } + private void checkPluginCompatibility(PluginInfo pluginInfo) throws Exception { + Version applicationVersion = Version.valueOf(Client.getApplicationVersion()); + Version pluginAppMinVersion = Version.valueOf(pluginInfo.getPluginAppMinVersion()); + + logger.log(Level.INFO, "Checking plugin compatibility:"); + logger.log(Level.INFO, "- Application version: " + Client.getApplicationVersion() + "(" + applicationVersion + ")"); + logger.log(Level.INFO, "- Plugin min. application version: " + pluginInfo.getPluginAppMinVersion() + "(" + pluginAppMinVersion + ")"); + + if (!applicationVersion.greaterThanOrEqualTo(pluginAppMinVersion)) { + throw new Exception("Plugin is incompatible to this application version. Plugin min. application version is " + + pluginInfo.getPluginAppMinVersion() + ", current application version is " + Client.getApplicationVersion()); + } + } + private String calculateChecksum(File tempPluginJarFile) throws Exception { CipherUtil.enableUnlimitedStrength(); @@ -196,9 +234,11 @@ private String calculateChecksum(File tempPluginJarFile) throws Exception { private PluginOperationResult executeInstallFromLocalFile(File pluginJarFile) throws Exception { PluginInfo pluginInfo = readPluginInfoFromJar(pluginJarFile); - File targetPluginJarFile = installPlugin(pluginJarFile, pluginInfo); checkPluginNotInstalled(pluginInfo.getPluginId()); + checkPluginCompatibility(pluginInfo); + + File targetPluginJarFile = installPlugin(pluginJarFile, pluginInfo); result.setSourcePluginPath(pluginJarFile.getPath()); result.setTargetPluginPath(targetPluginJarFile.getPath()); @@ -213,6 +253,7 @@ private PluginOperationResult executeInstallFromUrl(String downloadJarUrl) throw PluginInfo pluginInfo = readPluginInfoFromJar(tempPluginJarFile); checkPluginNotInstalled(pluginInfo.getPluginId()); + checkPluginCompatibility(pluginInfo); File targetPluginJarFile = installPlugin(tempPluginJarFile, pluginInfo); @@ -230,6 +271,8 @@ private void checkPluginNotInstalled(String pluginId) throws Exception { if (locallyInstalledPlugin != null) { throw new Exception("Plugin '" + pluginId + "' already installed. Use 'sy plugin remove " + pluginId + "' to uninstall it first."); } + + logger.log(Level.INFO, "Plugin '" + pluginId + "' not installed. Okay!"); } private PluginInfo readPluginInfoFromJar(File pluginJarFile) throws Exception { @@ -259,11 +302,13 @@ private PluginInfo readPluginInfoFromJar(File pluginJarFile) throws Exception { } private File installPlugin(File pluginJarFile, PluginInfo pluginInfo) throws IOException { - File globalUserPluginDir = Client.getUserPluginDir(); + File globalUserPluginDir = UserConfig.getUserPluginDir(); globalUserPluginDir.mkdirs(); File targetPluginJarFile = new File(globalUserPluginDir, String.format("syncany-plugin-%s-%s.jar", pluginInfo.getPluginId(), pluginInfo.getPluginVersion())); + + logger.log(Level.INFO, "Installing plugin from " + pluginJarFile + " to " + targetPluginJarFile + " ..."); FileUtils.copyFile(pluginJarFile, targetPluginJarFile); return targetPluginJarFile; diff --git a/syncany-lib/src/main/java/org/syncany/operations/watch/WatchOperation.java b/syncany-lib/src/main/java/org/syncany/operations/watch/WatchOperation.java index a3169c29..7f07fee7 100644 --- a/syncany-lib/src/main/java/org/syncany/operations/watch/WatchOperation.java +++ b/syncany-lib/src/main/java/org/syncany/operations/watch/WatchOperation.java @@ -73,8 +73,9 @@ public class WatchOperation extends Operation implements NotificationListenerLis private WatchOperationListener listener; private AtomicBoolean syncRunning; - private AtomicBoolean stopRequired; - private AtomicBoolean pauseRequired; + private AtomicBoolean syncRequested; + private AtomicBoolean stopRequested; + private AtomicBoolean pauseRequested; private RecursiveWatcher recursiveWatcher; private NotificationListener notificationListener; @@ -89,8 +90,9 @@ public WatchOperation(Config config, WatchOperationOptions options, WatchOperati this.listener = listener; this.syncRunning = new AtomicBoolean(false); - this.stopRequired = new AtomicBoolean(false); - this.pauseRequired = new AtomicBoolean(false); + this.syncRequested = new AtomicBoolean(false); + this.stopRequested = new AtomicBoolean(false); + this.pauseRequested = new AtomicBoolean(false); this.recursiveWatcher = null; this.notificationListener = null; @@ -109,8 +111,8 @@ public WatchOperationResult execute() throws Exception { startRecursiveWatcher(); } - while (!stopRequired.get()) { - while (pauseRequired.get()) { + while (!stopRequested.get()) { + while (pauseRequested.get()) { try { Thread.sleep(1000); } @@ -122,8 +124,10 @@ public WatchOperationResult execute() throws Exception { try { runSync(); - logger.log(Level.INFO, "Sync done, waiting {0} seconds ...", options.getInterval() / 1000); - Thread.sleep(options.getInterval()); + if (!syncRequested.get()) { + logger.log(Level.INFO, "Sync done, waiting {0} seconds ...", options.getInterval() / 1000); + Thread.sleep(options.getInterval()); + } } catch (Exception e) { logger.log(Level.INFO, String.format("Sync FAILED, waiting %d seconds ...", options.getInterval() / 1000), e); @@ -163,8 +167,9 @@ private void startNotificationListener() { private void runSync() throws Exception { if (!syncRunning.get()) { syncRunning.set(true); + syncRequested.set(false); - logger.log(Level.INFO, "Running sync ..."); + logger.log(Level.INFO, "RUNNING SYNC ..."); try { // Run down @@ -182,9 +187,14 @@ private void runSync() throws Exception { } } finally { + logger.log(Level.INFO, "SYNC DONE."); syncRunning.set(false); } } + else { + logger.log(Level.INFO, "Sync already running, setting 'sync requested' flag ..."); + syncRequested.set(true); + } } @Override @@ -216,15 +226,15 @@ private void notifyChanges() { } public void pause() { - pauseRequired.set(true); + pauseRequested.set(true); } public void resume() { - pauseRequired.set(false); + pauseRequested.set(false); } public void stop() { - stopRequired.set(true); + stopRequested.set(true); } /** diff --git a/syncany-lib/src/test/java/org/syncany/tests/scenarios/IgnoredFileScenarioTest.java b/syncany-lib/src/test/java/org/syncany/tests/scenarios/IgnoredFileScenarioTest.java index c188a156..392ca990 100644 --- a/syncany-lib/src/test/java/org/syncany/tests/scenarios/IgnoredFileScenarioTest.java +++ b/syncany-lib/src/test/java/org/syncany/tests/scenarios/IgnoredFileScenarioTest.java @@ -139,6 +139,44 @@ public void testIgnoredDirectory() throws Exception { assertFalse(clientB.getLocalFile("builds/test.txt").exists()); + // Tear down + clientA.deleteTestData(); + clientB.deleteTestData(); + TestFileUtil.deleteDirectory(tempDir); + } + + @Test + public void testIgnoredFileWildcard() throws Exception { + // Scenario: A ignores files using wildcards, creates it then ups, B should not have the file + + // Setup + File tempDir = TestFileUtil.createTempDirectoryInSystemTemp(); + + Connection testConnection = TestConfigUtil.createTestLocalConnection(); + TestClient clientA = new TestClient("A", testConnection); + TestClient clientB = new TestClient("B", testConnection); + + //Create ignore file and reload it + File syncanyIgnore = clientA.getLocalFile(Config.FILE_IGNORE); + TestFileUtil.createFileWithContent(syncanyIgnore, "*.bak\nignoredarchive.r??"); + clientA.getConfig().getIgnoredFiles().loadPatterns(); + + // A new/up + clientA.createNewFile("ignoredfile.bak"); + clientA.createNewFile("nonignoredfile.bar"); + clientA.createNewFile("ignoredarchive.r01"); + clientA.up(); + + clientB.down(); + + // The ignored file should not exist at B + assertTrue(clientA.getLocalFile("ignoredfile.bak").exists()); + assertFalse(clientB.getLocalFile("ignoredfile.bak").exists()); + assertTrue(clientA.getLocalFile("nonignoredfile.bar").exists()); + assertTrue(clientB.getLocalFile("nonignoredfile.bar").exists()); + assertTrue(clientA.getLocalFile("ignoredarchive.r01").exists()); + assertFalse(clientB.getLocalFile("ignoredarchive.r01").exists()); + // Tear down clientA.deleteTestData(); clientB.deleteTestData();