From 265708de847fdd56bfe7af1fd2dfa73ee3774d0c Mon Sep 17 00:00:00 2001 From: Vincent Wiencek Date: Thu, 6 Mar 2014 10:34:58 +0100 Subject: [PATCH] initial release --- .gitignore | 1 + .gitmodules | 3 + README.md | 14 +- build.gradle | 16 + core | 1 + settings.gradle | 5 + .../plugins/sftp/SftpConnection.java | 114 ++++++ .../connection/plugins/sftp/SftpPlugin.java | 54 +++ .../plugins/sftp/SftpTransferManager.java | 351 ++++++++++++++++++ .../plugin/sftp/EmbeddedSftpServerTest.java | 79 ++++ .../plugin/sftp/SftpConnectionPluginTest.java | 250 +++++++++++++ .../plugin/sftp/SftpTransferManagerTest.java | 69 ++++ 12 files changed, 954 insertions(+), 3 deletions(-) create mode 100644 .gitmodules create mode 100644 build.gradle create mode 160000 core create mode 100644 settings.gradle create mode 100644 src/main/java/org/syncany/connection/plugins/sftp/SftpConnection.java create mode 100644 src/main/java/org/syncany/connection/plugins/sftp/SftpPlugin.java create mode 100644 src/main/java/org/syncany/connection/plugins/sftp/SftpTransferManager.java create mode 100644 src/test/java/org/syncany/tests/connection/plugin/sftp/EmbeddedSftpServerTest.java create mode 100644 src/test/java/org/syncany/tests/connection/plugin/sftp/SftpConnectionPluginTest.java create mode 100644 src/test/java/org/syncany/tests/connection/plugin/sftp/SftpTransferManagerTest.java diff --git a/.gitignore b/.gitignore index 0f182a03..4d6556ce 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ *.jar *.war *.ear +/bin diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..9ccc4bcc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "core"] + path = core + url = http://github.com/vwiencek/syncany.git diff --git a/README.md b/README.md index 2425231c..62eb0945 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,12 @@ -syncany-plugin-sftp -=================== +Syncany SFTP Plugin -SFTP Syncany plugin +#1 Steps to make it work + + git clone http://github.com/vwiencek/syncany-plugin-sftp.git + cd syncany-plugin-hazelcast + git submodule init + git submodule update + gradle eclipse + +Then you have a fully 'eclipse-prepared' environment to start improving the plugin + diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..63a44a9e --- /dev/null +++ b/build.gradle @@ -0,0 +1,16 @@ +apply plugin: 'java' +apply plugin: 'eclipse' + +dependencies { + compile project(':syncany-lib') + compile "com.jcraft:jsch:0.1.50" + compile "org.apache.sshd:sshd-core:0.9.0" + compile "org.apache.commons:commons-vfs2:2.0" + + testCompile "junit:junit:4.3" + testCompile project(path : ':syncany-lib', configuration: 'tests') +} + +repositories { + mavenCentral() +} diff --git a/core b/core new file mode 160000 index 00000000..02402358 --- /dev/null +++ b/core @@ -0,0 +1 @@ +Subproject commit 02402358f6381c44a20b3e53ff2cf193e030fc83 diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..b28f86e8 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,5 @@ +include 'syncany-lib' +include 'syncany-util' + +project(':syncany-lib').projectDir = new File('core/syncany-lib') +project(':syncany-util').projectDir = new File('core/syncany-util') diff --git a/src/main/java/org/syncany/connection/plugins/sftp/SftpConnection.java b/src/main/java/org/syncany/connection/plugins/sftp/SftpConnection.java new file mode 100644 index 00000000..1698d566 --- /dev/null +++ b/src/main/java/org/syncany/connection/plugins/sftp/SftpConnection.java @@ -0,0 +1,114 @@ +/* + * 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.connection.plugins.sftp; + +import java.util.Map; + +import org.syncany.connection.plugins.Connection; +import org.syncany.connection.plugins.PluginOptionSpec; +import org.syncany.connection.plugins.PluginOptionSpec.ValueType; +import org.syncany.connection.plugins.PluginOptionSpecs; +import org.syncany.connection.plugins.StorageException; +import org.syncany.connection.plugins.TransferManager; + +/** + * The SFTP connection represents the settings required to connect to an + * SFTP-based storage backend. It can be used to initialize/create an + * {@link SftpTransferManager} and is part of the {@link SftpPlugin}. + * + * @author Vincent Wiencek + */ +public class SftpConnection implements Connection { + private String hostname; + private String username; + private String password; + private String path; + private int port; + + @Override + public TransferManager createTransferManager() { + return new SftpTransferManager(this); + } + + public String getHostname() { + return hostname; + } + + public void setHostname(String hostname) { + this.hostname = hostname; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public int getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + @Override + public void init(Map optionValues) throws StorageException { + getOptionSpecs().validate(optionValues); + this.hostname = optionValues.get("hostname"); + this.username = optionValues.get("username"); + this.password = optionValues.get("password"); + this.path = optionValues.get("path"); + this.port = Integer.parseInt(optionValues.get("port")); + } + + @Override + public PluginOptionSpecs getOptionSpecs() { + return new PluginOptionSpecs( + new PluginOptionSpec("hostname", "Hostname", ValueType.STRING, true, false, null), + new PluginOptionSpec("username", "Username", ValueType.STRING, true, false, null), + new PluginOptionSpec("password", "Password", ValueType.STRING, true, true, null), + new PluginOptionSpec("path", "Path", ValueType.STRING, true, false, null), + new PluginOptionSpec("port", "Port", ValueType.INT, false, false, "22") + ); + } + + @Override + public String toString() { + return SftpConnection.class.getSimpleName() + + "[hostname=" + hostname + ":" + port + ", username=" + username + ", path=" + path + "]"; + } +} diff --git a/src/main/java/org/syncany/connection/plugins/sftp/SftpPlugin.java b/src/main/java/org/syncany/connection/plugins/sftp/SftpPlugin.java new file mode 100644 index 00000000..ec976d17 --- /dev/null +++ b/src/main/java/org/syncany/connection/plugins/sftp/SftpPlugin.java @@ -0,0 +1,54 @@ +/* + * 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.connection.plugins.sftp; + +import org.syncany.connection.plugins.Connection; +import org.syncany.connection.plugins.Plugin; + +/** + * Identifies the SFTP-based storage {@link Plugin} for Syncany. + * + *

This class defines the identifier, name and + * version of the plugin. It furthermore allows the instantiation + * of a plugin-specific {@link SftpConnection}. + * + * @author Vincent Wiencek + */ +public class SftpPlugin extends Plugin { + public static final String ID = "sftp"; + + @Override + public String getId() { + return ID; + } + + @Override + public String getName() { + return "SFTP"; + } + + @Override + public Integer[] getVersion() { + return new Integer[] { 0, 1 }; + } + + @Override + public Connection createConnection() { + return new SftpConnection(); + } +} diff --git a/src/main/java/org/syncany/connection/plugins/sftp/SftpTransferManager.java b/src/main/java/org/syncany/connection/plugins/sftp/SftpTransferManager.java new file mode 100644 index 00000000..209901ba --- /dev/null +++ b/src/main/java/org/syncany/connection/plugins/sftp/SftpTransferManager.java @@ -0,0 +1,351 @@ +/* + * 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.connection.plugins.sftp; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.apache.commons.io.FileUtils; +import org.syncany.connection.plugins.AbstractTransferManager; +import org.syncany.connection.plugins.DatabaseRemoteFile; +import org.syncany.connection.plugins.MultiChunkRemoteFile; +import org.syncany.connection.plugins.RemoteFile; +import org.syncany.connection.plugins.StorageException; +import org.syncany.connection.plugins.TransferManager; +import org.syncany.util.FileUtil; + +import com.jcraft.jsch.ChannelSftp; +import com.jcraft.jsch.ChannelSftp.LsEntry; +import com.jcraft.jsch.ChannelSftp.LsEntrySelector; +import com.jcraft.jsch.JSch; +import com.jcraft.jsch.Session; +import com.jcraft.jsch.SftpATTRS; +import com.jcraft.jsch.SftpException; + +/** + * Implements a {@link TransferManager} based on an SFTP storage backend for the + * {@link SftpPlugin}. + * + *

Using an {@link SftpConnection}, the transfer manager is configured and uses + * a well defined SFTP folder to store the Syncany repository data. While repo and + * master file are stored in the given folder, databases and multichunks are stored + * in special sub-folders: + * + *

    + *
  • The databases folder keeps all the {@link DatabaseRemoteFile}s
  • + *
  • The multichunks folder keeps the actual data within the {@link MultiChunkRemoteFile}s
  • + *
+ * + *

All operations are auto-connected, i.e. a connection is automatically + * established. Connecting is retried a few times before throwing an exception. + * + * @author Vincent Wiencek + */ +public class SftpTransferManager extends AbstractTransferManager { + private static final Logger logger = Logger.getLogger(SftpTransferManager.class.getSimpleName()); + + private static final int CONNECT_RETRY_COUNT = 3; + + private JSch jsch; + private Session session; + private ChannelSftp channel; + + private String repoPath; + private String multichunkPath; + private String databasePath; + + public SftpTransferManager(SftpConnection connection) { + super(connection); + + this.jsch = new JSch(); + this.repoPath = connection.getPath(); + this.multichunkPath = connection.getPath() + "/multichunks"; + this.databasePath = connection.getPath() + "/databases"; + } + + @Override + public SftpConnection getConnection() { + return (SftpConnection) super.getConnection(); + } + + @Override + public void connect() throws StorageException { + for (int i = 0; i < CONNECT_RETRY_COUNT; i++) { + try { + if (session != null && session.isConnected()) { + return; + } + + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, "SFTP client connecting to {0}:{1} ...", new Object[] { getConnection().getHostname(), getConnection().getPort() }); + } + + Properties properties = new Properties(); + properties.put("StrictHostKeyChecking", "no"); + session = jsch.getSession(getConnection().getUsername(), getConnection().getHostname(), getConnection().getPort()); + session.setConfig(properties); + session.setPassword(getConnection().getPassword()); + session.connect(); + if (!session.isConnected()){ + logger.warning("SFTP: unable to connect to sftp host " + getConnection().getHostname() + ":" + getConnection().getPort()); + } + + channel = (ChannelSftp)session.openChannel("sftp"); + channel.connect(); + if (!channel.isConnected()){ + logger.warning("SFTP: unable to connect to sftp channel " + getConnection().getHostname() + ":" + getConnection().getPort()); + } + return; + } + catch (Exception ex) { + if (i == CONNECT_RETRY_COUNT - 1) { + logger.log(Level.WARNING, "SFTP client connection failed. Retrying failed.", ex); + throw new StorageException(ex); + } + else { + logger.log(Level.WARNING, "SFTP client connection failed. Retrying " + (i + 1) + "/" + CONNECT_RETRY_COUNT + " ...", ex); + } + } + } + } + + @Override + public void disconnect() { + if (channel != null){ + channel.quit(); + channel.disconnect(); + } + if (session != null){ + session.disconnect(); + } + } + + @Override + public void init(boolean createIfRequired) throws StorageException { + connect(); + + try { + if (!repoExists() && createIfRequired) { + channel.mkdir(repoPath); + } + channel.mkdir(multichunkPath); + channel.mkdir(databasePath); + } + catch (SftpException e) { + disconnect(); + throw new StorageException("Cannot create directory " + multichunkPath + ", or " + databasePath, e); + } + } + + @Override + public void download(RemoteFile remoteFile, File localFile) throws StorageException { + connect(); + + String remotePath = getRemoteFile(remoteFile); + + if (!remoteFile.getName().equals(".") && !remoteFile.getName().equals("..")){ + try { + // Download file + File tempFile = createTempFile(localFile.getName()); + OutputStream tempFOS = new FileOutputStream(tempFile); + + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, "SFTP: Downloading {0} to temp file {1}", new Object[] { remotePath, tempFile }); + } + + channel.get(remotePath, tempFOS); + + tempFOS.close(); + + // Move file + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, "SFTP: Renaming temp file {0} to file {1}", new Object[] { tempFile, localFile }); + } + + localFile.delete(); + FileUtils.moveFile(tempFile, localFile); + tempFile.delete(); + } + catch (SftpException | IOException ex) { + disconnect(); + logger.log(Level.SEVERE, "Error while downloading file " + remoteFile.getName(), ex); + throw new StorageException(ex); + } + } + } + + @Override + public void upload(File localFile, RemoteFile remoteFile) throws StorageException { + connect(); + + String remotePath = getRemoteFile(remoteFile); + String tempRemotePath = getConnection().getPath() + "/temp-" + remoteFile.getName(); + + try { + // Upload to temp file + InputStream fileFIS = new FileInputStream(localFile); + + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, "SFTP: Uploading {0} to temp file {1}", new Object[] { localFile, tempRemotePath }); + } + + channel.put(fileFIS, tempRemotePath); + + fileFIS.close(); + + // Move + if (logger.isLoggable(Level.INFO)) { + logger.log(Level.INFO, "SFTP: Renaming temp file {0} to file {1}", new Object[] { tempRemotePath, remotePath }); + } + + channel.rename(tempRemotePath, remotePath); + } + catch (SftpException | IOException ex) { + disconnect(); + logger.log(Level.SEVERE, "Could not upload file " + localFile + " to " + remoteFile.getName(), ex); + throw new StorageException(ex); + } + } + + @Override + public boolean delete(RemoteFile remoteFile) throws StorageException { + connect(); + + String remotePath = getRemoteFile(remoteFile); + + try { + channel.rm(remotePath); + return true; + } + catch (SftpException ex) { + disconnect(); + logger.log(Level.SEVERE, "Could not delete file " + remoteFile.getName(), ex); + throw new StorageException(ex); + } + } + + @Override + public Map list(Class remoteFileClass) throws StorageException { + connect(); + + try { + // List folder + String remoteFilePath = getRemoteFilePath(remoteFileClass); + + List entries = listEntries(remoteFilePath + "/"); + + // Create RemoteFile objects + Map remoteFiles = new HashMap(); + + for (LsEntry entry : entries) { + try { + T remoteFile = RemoteFile.createRemoteFile(entry.getFilename(), remoteFileClass); + remoteFiles.put(entry.getFilename(), remoteFile); + } + catch (Exception e) { + logger.log(Level.INFO, "Cannot create instance of " + remoteFileClass.getSimpleName() + " for file " + entry.getFilename() + "; maybe invalid file name pattern. Ignoring file."); + } + } + + return remoteFiles; + } + catch (SftpException ex) { + disconnect(); + + logger.log(Level.SEVERE, "Unable to list FTP directory.", ex); + throw new StorageException(ex); + } + } + + private String getRemoteFile(RemoteFile remoteFile) { + return getRemoteFilePath(remoteFile.getClass()) + "/" + remoteFile.getName(); + } + + private String getRemoteFilePath(Class remoteFile) { + if (remoteFile.equals(MultiChunkRemoteFile.class)) { + return multichunkPath; + } + else if (remoteFile.equals(DatabaseRemoteFile.class)) { + return databasePath; + } + else { + return repoPath; + } + } + + private List listEntries(String absolutePath) throws SftpException{ + final List result = new ArrayList<>(); + LsEntrySelector selector = new LsEntrySelector(){ + public int select(LsEntry entry){ + if (!entry.getFilename().equals(".") && !entry.getFilename().equals("..")){ + result.add(entry); + } + return CONTINUE; + } + }; + channel.ls(absolutePath, selector); + return result; + } + + @Override + public boolean repoExists() throws StorageException { + try { + SftpATTRS attrs = channel.stat(repoPath); + return attrs.isDir(); + } + catch (Exception e) { + return false; + } + } + + @Override + public boolean repoIsEmpty() throws StorageException { + try { + return channel.ls(repoPath).size() == 2; // "." and ".." + } + catch (SftpException e) { + throw new StorageException(e.getMessage()); + } + } + + @Override + public boolean hasWriteAccess() throws StorageException { + try { + String parentPath = FileUtil.getUnixParentPath(repoPath); + SftpATTRS stat = channel.stat(parentPath); + return stat != null && ((stat.getPermissions() & 00200) != 0) && stat.getUId() != 0; + } + catch (SftpException ex) { + if (ex.id == 3 /* access denied */ || ex.id == 2 /* file not found */) { + return false; + } + throw new StorageException(ex.getMessage()); + } + } +} diff --git a/src/test/java/org/syncany/tests/connection/plugin/sftp/EmbeddedSftpServerTest.java b/src/test/java/org/syncany/tests/connection/plugin/sftp/EmbeddedSftpServerTest.java new file mode 100644 index 00000000..f2d79036 --- /dev/null +++ b/src/test/java/org/syncany/tests/connection/plugin/sftp/EmbeddedSftpServerTest.java @@ -0,0 +1,79 @@ +/* + * 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.tests.connection.plugin.sftp; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +import org.apache.sshd.SshServer; +import org.apache.sshd.common.NamedFactory; +import org.apache.sshd.common.file.nativefs.NativeFileSystemFactory; +import org.apache.sshd.server.Command; +import org.apache.sshd.server.PasswordAuthenticator; +import org.apache.sshd.server.UserAuth; +import org.apache.sshd.server.auth.UserAuthPassword; +import org.apache.sshd.server.command.ScpCommandFactory; +import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider; +import org.apache.sshd.server.session.ServerSession; +import org.apache.sshd.server.sftp.SftpSubsystem; + +/** + * @author vincent + * + */ +public class EmbeddedSftpServerTest { + public static int PORT = 2338; + public static String HOST = "127.0.0.1"; + + private static SshServer sshd; + + public static void stopServer() throws InterruptedException { + if (sshd != null) { + sshd.stop(); + } + } + + public static void startServer() throws IOException { + File hostKeyFile = File.createTempFile("hostkey", "ser"); + + sshd = SshServer.setUpDefaultServer(); + sshd.setPort(PORT); + sshd.setKeyPairProvider(new SimpleGeneratorHostKeyProvider(hostKeyFile.getAbsolutePath())); + sshd.setFileSystemFactory(new NativeFileSystemFactory()); + + List> userAuthFactories = new ArrayList>(); + userAuthFactories.add(new UserAuthPassword.Factory()); + sshd.setUserAuthFactories(userAuthFactories); + + sshd.setCommandFactory(new ScpCommandFactory()); + + List> namedFactoryList = new ArrayList>(); + namedFactoryList.add(new SftpSubsystem.Factory()); + sshd.setSubsystemFactories(namedFactoryList); + sshd.setPasswordAuthenticator(new PasswordAuthenticator() { + @Override + public boolean authenticate(String username, String password, ServerSession session) { + return true; + } + }); + + sshd.start(); + } +} diff --git a/src/test/java/org/syncany/tests/connection/plugin/sftp/SftpConnectionPluginTest.java b/src/test/java/org/syncany/tests/connection/plugin/sftp/SftpConnectionPluginTest.java new file mode 100644 index 00000000..d35e8b9b --- /dev/null +++ b/src/test/java/org/syncany/tests/connection/plugin/sftp/SftpConnectionPluginTest.java @@ -0,0 +1,250 @@ +/* + * 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.tests.connection.plugin.sftp; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.After; +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.BeforeClass; +import org.junit.Test; +import org.syncany.connection.plugins.Connection; +import org.syncany.connection.plugins.Plugin; +import org.syncany.connection.plugins.Plugins; +import org.syncany.connection.plugins.RemoteFile; +import org.syncany.connection.plugins.StorageException; +import org.syncany.connection.plugins.TransferManager; +import org.syncany.connection.plugins.sftp.SftpConnection; +import org.syncany.connection.plugins.sftp.SftpPlugin; +import org.syncany.connection.plugins.sftp.SftpTransferManager; +import org.syncany.tests.util.TestFileUtil; + +public class SftpConnectionPluginTest { + private static File tempLocalSourceDir; + + private Map sshPluginSettings; + + @BeforeClass + public static void beforeTestSetup() { + try { + EmbeddedSftpServerTest.startServer(); + } + catch (IOException e) { + e.printStackTrace(); + } + } + + @AfterClass + public static void tearDown() { + try { + EmbeddedSftpServerTest.stopServer(); + } + catch (InterruptedException e) { + e.printStackTrace(); + } + } + + @Before + public void setUp() throws Exception { + File rootDir = TestFileUtil.createTempDirectoryInSystemTemp(); + + tempLocalSourceDir = new File(rootDir+"/local"); + tempLocalSourceDir.mkdir(); + + sshPluginSettings = new HashMap(); + sshPluginSettings.put("hostname", EmbeddedSftpServerTest.HOST); + sshPluginSettings.put("username", "user"); + sshPluginSettings.put("password", "pass"); + sshPluginSettings.put("port", "" + EmbeddedSftpServerTest.PORT); + sshPluginSettings.put("path", "/repo"); + } + + @After + public void tear(){ + TestFileUtil.deleteDirectory(tempLocalSourceDir); + } + + @Test + public void testLoadPluginAndCreateTransferManager() throws StorageException { + loadPluginAndCreateTransferManager(); + } + + @Test + public void testLocalPluginInfo() { + Plugin pluginInfo = Plugins.get("sftp"); + + assertNotNull("PluginInfo should not be null.", pluginInfo); + assertEquals("Plugin ID should be 'ssh'.", "sftp", pluginInfo.getId()); + assertNotNull("Plugin version should not be null.", pluginInfo.getVersion()); + assertNotNull("Plugin name should not be null.", pluginInfo.getName()); + } + + @Test(expected=StorageException.class) + public void testConnectToNonExistantFolder() throws StorageException { + Plugin pluginInfo = Plugins.get("sftp"); + + Map invalidPluginSettings = new HashMap(); + invalidPluginSettings.put("hostname", EmbeddedSftpServerTest.HOST); + invalidPluginSettings.put("username", "user"); + invalidPluginSettings.put("password", "pass"); + invalidPluginSettings.put("port", "" + EmbeddedSftpServerTest.PORT); + invalidPluginSettings.put("path", "/path/does/not/exist"); + + Connection connection = pluginInfo.createConnection(); + connection.init(invalidPluginSettings); + + TransferManager transferManager = connection.createTransferManager(); + + // This should cause a Storage exception, because the path does not exist + transferManager.connect(); + transferManager.init(true); + } + + @Test(expected=StorageException.class) + public void testConnectWithInvalidSettings() throws StorageException { + Plugin pluginInfo = Plugins.get("sftp"); + + Map invalidPluginSettings = new HashMap(); + + Connection connection = pluginInfo.createConnection(); + connection.init(invalidPluginSettings); + + TransferManager transferManager = connection.createTransferManager(); + + // This should cause a Storage exception, because the path does not exist + transferManager.connect(); + } + + @Test + public void testSftpUploadListDownloadAndDelete() throws Exception { + // Generate test files + Map inputFiles = generateTestInputFile(); + + // Create connection, upload, list, download + TransferManager transferManager = loadPluginAndCreateTransferManager(); + transferManager.connect(); + + Map uploadedFiles = uploadChunkFiles(transferManager, inputFiles.values()); + Map remoteFiles = transferManager.list(RemoteFile.class); + Map downloadedLocalFiles = downloadRemoteFiles(transferManager, remoteFiles.values()); + + // Compare + assertEquals("Number of uploaded files should be the same as the input files.", uploadedFiles.size(), remoteFiles.size()); + assertEquals("Number of remote files should be the same as the downloaded files.", remoteFiles.size(), downloadedLocalFiles.size()); + + for (Map.Entry inputFileEntry : inputFiles.entrySet()) { + File inputFile = inputFileEntry.getValue(); + + RemoteFile uploadedFile = uploadedFiles.get(inputFile); + File downloadedLocalFile = downloadedLocalFiles.get(uploadedFile); + + assertNotNull("Cannot be null.", uploadedFile); + assertNotNull("Cannot be null.", downloadedLocalFile); + + byte[] checksumOriginalFile = TestFileUtil.createChecksum(inputFile); + byte[] checksumDownloadedFile = TestFileUtil.createChecksum(downloadedLocalFile); + + assertArrayEquals("Uploaded file differs from original file.", checksumOriginalFile, checksumDownloadedFile); + } + + // Delete + for (RemoteFile remoteFileToDelete : uploadedFiles.values()) { + transferManager.delete(remoteFileToDelete); + } + + Map remoteFiles2 = transferManager.list(RemoteFile.class); + Map downloadedLocalFiles2 = downloadRemoteFiles(transferManager, remoteFiles2.values()); + + for (RemoteFile remoteFileToBeDeleted : downloadedLocalFiles2.keySet()) { + assertFalse("Could not delete remote file.",downloadedLocalFiles2.containsKey(remoteFileToBeDeleted)); + } + } + + @Test(expected=StorageException.class) + public void testDeleteNonExistantFile() throws StorageException { + TransferManager transferManager = loadPluginAndCreateTransferManager(); + transferManager.connect(); + transferManager.delete(new RemoteFile("non-existant-file")); + } + + private Map generateTestInputFile() throws IOException { + Map inputFilesMap = new HashMap(); + List inputFiles = TestFileUtil.createRandomFilesInDirectory(tempLocalSourceDir, 50*1024, 10); + + for (File file : inputFiles) { + inputFilesMap.put(file.getName(), file); + } + + return inputFilesMap; + } + + private Map uploadChunkFiles(TransferManager transferManager, Collection inputFiles) throws StorageException { + Map inputFileOutputFile = new HashMap(); + + for (File inputFile : inputFiles) { + RemoteFile remoteOutputFile = new RemoteFile(inputFile.getName()); + transferManager.upload(inputFile, remoteOutputFile); + + inputFileOutputFile.put(inputFile, remoteOutputFile); + } + + return inputFileOutputFile; + } + + private Map downloadRemoteFiles(TransferManager transferManager, Collection remoteFiles) throws StorageException { + Map downloadedLocalFiles = new HashMap(); + + for (RemoteFile remoteFile : remoteFiles) { + File downloadedLocalFile = new File(tempLocalSourceDir+"/downloaded-"+remoteFile.getName()); + transferManager.download(remoteFile, downloadedLocalFile); + + downloadedLocalFiles.put(remoteFile, downloadedLocalFile); + + assertTrue("Downloaded file does not exist.", downloadedLocalFile.exists()); + } + + return downloadedLocalFiles; + } + + private TransferManager loadPluginAndCreateTransferManager() throws StorageException { + Plugin pluginInfo = Plugins.get("sftp"); + + Connection connection = pluginInfo.createConnection(); + connection.init(sshPluginSettings); + + TransferManager transferManager = connection.createTransferManager(); + + assertEquals("SftpPlugin expected.", SftpPlugin.class, pluginInfo.getClass()); + assertEquals("SftpConnection expected.", SftpConnection.class, connection.getClass()); + assertEquals("SftpTransferManager expected.", SftpTransferManager.class, transferManager.getClass()); + + return transferManager; + } +} diff --git a/src/test/java/org/syncany/tests/connection/plugin/sftp/SftpTransferManagerTest.java b/src/test/java/org/syncany/tests/connection/plugin/sftp/SftpTransferManagerTest.java new file mode 100644 index 00000000..1de7be6b --- /dev/null +++ b/src/test/java/org/syncany/tests/connection/plugin/sftp/SftpTransferManagerTest.java @@ -0,0 +1,69 @@ +/* + * 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.tests.connection.plugin.sftp; + +import junit.framework.Assert; + +import org.junit.Test; +import org.syncany.connection.plugins.StorageException; +import org.syncany.connection.plugins.TransferManager.StorageTestResult; +import org.syncany.connection.plugins.sftp.SftpConnection; + +/** + * @author Vincent Wiencek + * + */ +public class SftpTransferManagerTest { + private final static String SANDBOX = "/home/xxxxxxxxxxx/"; + private final static String USERNAME = "xxxxxxxx"; + private final static String PASSWORD = "xxxxxxx"; + private final static String HOST = "xxxxxxxxxx"; + + @Test + public void testSftpTransferManager() throws StorageException { + Assert.assertEquals(StorageTestResult.NO_REPO, test(SANDBOX + "repoValid")); + Assert.assertEquals(StorageTestResult.NO_REPO, test(SANDBOX + "emptyRepo")); + Assert.assertEquals(StorageTestResult.NO_REPO_CANNOT_CREATE, test(SANDBOX + "canNotWrite/inside")); + Assert.assertEquals(StorageTestResult.REPO_EXISTS, test(SANDBOX + "notEmptyRepo")); + } + + public StorageTestResult test(String host, String path) throws StorageException{ + SftpConnection cnx = con(host); + cnx.setPath(path); + return cnx.createTransferManager().test(); + } + + public StorageTestResult test(String path) throws StorageException{ + SftpConnection cnx = con(); + cnx.setPath(path); + return cnx.createTransferManager().test(); + } + + public SftpConnection con(){ + return con(HOST); + } + + public SftpConnection con(String host){ + SftpConnection connection = new SftpConnection(); + connection.setHostname(host); + connection.setPort(22); + connection.setUsername(USERNAME); + connection.setPassword(PASSWORD); + return connection; + } +}