Skip to content

Commit

Permalink
Fix #91 Local copying for files over same connection
Browse files Browse the repository at this point in the history
  • Loading branch information
hierynomus committed Jan 28, 2014
1 parent 9a85d94 commit 3f798fc
Show file tree
Hide file tree
Showing 5 changed files with 277 additions and 17 deletions.
23 changes: 23 additions & 0 deletions src/main/java/com/xebialabs/overthere/ConnectionOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,29 @@ public class ConnectionOptions {
*/
public static final String JUMPSTATION = "jumpstation";

/**
* Common connection option (<code>ConnectionOptions</code>) that specifies the command to execute when doing a local copy on the remote host.
*/
public static final String LOCAL_COPY_COMMAND = "localCopyCommand";

/**
* Default value (<code>cp -pr {0} {1}</code>) for the common connection option that specifies the command to execute on a UNIX host
* when doing a local copy.
*/
public static final String LOCAL_COPY_COMMAND_UNIX_DEFAULT_VALUE = "cp -pRf {0} {1}";

/**
* Default value (<code>cp -pr {0} {1}</code>) for the common connection option that specifies the command to execute on a Z/OS host
* when doing a local copy.
*/
public static final String LOCAL_COPY_COMMAND_ZOS_DEFAULT_VALUE = "cp -pRf {0} {1}";

/**
* Default value (<code>copy {0} {1}</code>) for the common connection option that specifies the command to execute on a Windows host
* when doing a local copy.
*/
public static final String LOCAL_COPY_COMMAND_WINDOWS_DEFAULT_VALUE = "xcopy {0} {1} /S /E /Y /O /X /Q /I /K /R /H";

private final Map<String, Object> options;

private static final ImmutableSet<String> filteredKeys = ImmutableSet.of(PASSWORD, PASSPHRASE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,20 @@ public final boolean canStartProcess() {

private static Logger logger = LoggerFactory.getLogger(BaseOverthereConnection.class);

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;

final BaseOverthereConnection that = (BaseOverthereConnection) o;

return options.equals(that.options) && protocol.equals(that.protocol);
}

@Override
public int hashCode() {
int result = protocol.hashCode();
result = 31 * result + options.hashCode();
return result;
}
}
47 changes: 44 additions & 3 deletions src/main/java/com/xebialabs/overthere/spi/BaseOverthereFile.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@
*/
package com.xebialabs.overthere.spi;

import com.xebialabs.overthere.OverthereFile;
import com.xebialabs.overthere.RuntimeIOException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.xebialabs.overthere.*;
import com.xebialabs.overthere.util.OverthereFileCopier;

import static com.google.common.base.Preconditions.checkArgument;
Expand Down Expand Up @@ -69,17 +71,56 @@ public void deleteRecursively() throws RuntimeIOException {
public final void copyTo(final OverthereFile dest) {
checkArgument(dest instanceof BaseOverthereFile<?>, "dest is not a subclass of BaseOverthereFile");

((BaseOverthereFile<?>) dest).copyFrom(this);
if (getConnection().equals(dest.getConnection())) {
((BaseOverthereFile<?>) dest).localCopyFrom(this);
} else {
((BaseOverthereFile<?>) dest).copyFrom(this);
}
}

protected void copyFrom(OverthereFile source) {
OverthereFileCopier.copy(source, this);
}

/**
* Copies this file or directory (recursively) to a (new) destination in the same connection.
* @param source The source file or directory
*/
protected void localCopyFrom(OverthereFile source) {
checkArgument(!exists() || (source.isDirectory() == isDirectory()), "Cannot local copy files into directories or vice-versa for [source %s %s to destination %s %s]", typeOf(source), source.getPath(), typeOf(this), getPath());
OperatingSystemFamily hostOperatingSystem = source.getConnection().getHostOperatingSystem();
CmdLine cmdLine = new CmdLine();
String defaultValue = null;
switch (hostOperatingSystem) {
case WINDOWS:
defaultValue = ConnectionOptions.LOCAL_COPY_COMMAND_WINDOWS_DEFAULT_VALUE;
break;
case UNIX:
defaultValue = ConnectionOptions.LOCAL_COPY_COMMAND_UNIX_DEFAULT_VALUE;
break;
case ZOS:
defaultValue = ConnectionOptions.LOCAL_COPY_COMMAND_ZOS_DEFAULT_VALUE;
break;
}

String commandTemplate = getConnection().getOptions().get(ConnectionOptions.LOCAL_COPY_COMMAND, defaultValue);
cmdLine.addTemplatedFragment(commandTemplate, source.getPath(), getPath());

logger.debug("Going to execute command [{}] on [{}]", cmdLine, source.getConnection());
source.getConnection().execute(cmdLine);
}

private String typeOf(OverthereFile overthereFile) {
return overthereFile.isDirectory() ? "directory" : "file";
}


/**
* Subclasses MUST implement toString properly.
*/
@Override
public abstract String toString();

private static final Logger logger = LoggerFactory.getLogger(BaseOverthereFile.class);

}
123 changes: 109 additions & 14 deletions src/test/java/com/xebialabs/overthere/OverthereConnectionItestBase.java
Original file line number Diff line number Diff line change
Expand Up @@ -522,22 +522,126 @@ public void shouldCreatePopulateListAndRemoveTemporaryDirectory() {
assertThat("Expected temporary directory to not exist after removing it when it was empty", tempDir.exists(), equalTo(false));
}

@Test
public void shouldCopyRemoteFileToRemoteLocationOnSameConnection() throws IOException {
File smallFile = temp.newFile("small.dat");
writeRandomBytes(smallFile, SMALL_FILE_SIZE);

OverthereFile remoteSmallFile = connection.getTempFile("small.dat");
LocalFile.valueOf(smallFile).copyTo(remoteSmallFile);

OverthereFile remoteSmallCopy = connection.getTempFile("copy-of-small.dat");
remoteSmallFile.copyTo(remoteSmallCopy);

assertThat(remoteSmallCopy.exists(), equalTo(true));
}

@Test
public void shouldCopyRemoteDirectoryToRemoteLocationOnSameConnection() throws IOException {
File directory = temp.newFolder("folder");
File smallFile = new File(directory, "small.dat");
writeRandomBytes(smallFile, SMALL_FILE_SIZE);

OverthereFile remoteDir = connection.getTempFile("remote-dir");
LocalFile.valueOf(directory).copyTo(remoteDir);

assertThat(remoteDir.exists(), equalTo(true));
assertThat(remoteDir.getFile("small.dat").exists(), equalTo(true));

OverthereFile remoteCopy = connection.getTempFile("remote-dir-copy");
remoteDir.copyTo(remoteCopy);

assertThat(remoteCopy.exists(), equalTo(true));
assertThat(remoteCopy.getFile("small.dat").exists(), equalTo(true));
}

@Test
public void shouldCopyRemoteFileOverRemoteExistingFileOnSameConnection() throws IOException {
File smallFile = temp.newFile("small.dat");
byte[] bytes = writeRandomBytes(smallFile, SMALL_FILE_SIZE);

OverthereFile remoteSmallFile = connection.getTempFile("small.dat");
LocalFile.valueOf(smallFile).copyTo(remoteSmallFile);

OverthereFile remoteSmallCopy = connection.getTempFile("copy-of-small.dat");
writeData(remoteSmallCopy, "I exist".getBytes());
remoteSmallFile.copyTo(remoteSmallCopy);

assertThat(readBytes(remoteSmallCopy, SMALL_FILE_SIZE), equalTo(bytes));
}

@Test
public void shouldCopyRemoteDirectoryOverRemoteExistingDirectoryOnSameConnection() throws IOException {
File directory = temp.newFolder("folder");
File smallFile = new File(directory, "small.dat");
byte[] bytes = writeRandomBytes(smallFile, SMALL_FILE_SIZE);

OverthereFile remoteDir = connection.getTempFile("remote-dir");
LocalFile.valueOf(directory).copyTo(remoteDir);

OverthereFile remoteCopy = connection.getTempFile("remote-dir-copy");
remoteCopy.mkdir();
OverthereFile remoteSmallCopy = remoteCopy.getFile("small.dat");
writeData(remoteSmallCopy, "I exist".getBytes());

// Check that we haven't copied into, but over.
assertThat(remoteCopy.getFile("remote-dir").exists(), equalTo(false));

remoteDir.copyTo(remoteCopy);
assertThat(readBytes(remoteSmallCopy, SMALL_FILE_SIZE), equalTo(bytes));
}

@Test(expectedExceptions = IllegalArgumentException.class)
public void shouldNotCopyRemoteFileIntoRemoteExistingDirectoryOnSameConnection() throws IOException {
File smallFile = temp.newFile("small.dat");
writeRandomBytes(smallFile, SMALL_FILE_SIZE);

OverthereFile remoteSmallFile = connection.getTempFile("small.dat");
LocalFile.valueOf(smallFile).copyTo(remoteSmallFile);

OverthereFile dir = connection.getTempFile("dir");
dir.mkdir();
remoteSmallFile.copyTo(dir);
fail("Should not succeed copying file to directory in local mode");
}

@Test(expectedExceptions = IllegalArgumentException.class)
public void shouldNotCopyRemoteDirectoryOntoExistingFileOnSameConnection() throws IOException {
File directory = temp.newFolder("folder");
File smallFile = new File(directory, "small.dat");
byte[] bytes = writeRandomBytes(smallFile, SMALL_FILE_SIZE);

OverthereFile remoteDir = connection.getTempFile("remote-dir");
LocalFile.valueOf(directory).copyTo(remoteDir);

OverthereFile tempFile = connection.getTempFile("file.dat");
writeData(tempFile, "Hello!".getBytes());

remoteDir.copyTo(tempFile);
fail("Should not succeed copying directory to file in local mode");
}

@Test
public void shouldWriteLargeFile() throws IOException {
byte[] largeFileContentsWritten = generateRandomBytes(LARGE_FILE_SIZE);

OverthereFile remoteLargeFile = connection.getTempFile("large.dat");
OverthereUtils.write(largeFileContentsWritten, remoteLargeFile);

byte[] largeFileContentsRead = new byte[LARGE_FILE_SIZE];
byte[] largeFileContentsRead = readBytes(remoteLargeFile, LARGE_FILE_SIZE);

assertThat(largeFileContentsRead, equalTo(largeFileContentsWritten));
}

private byte[] readBytes(OverthereFile remoteLargeFile, int size) throws IOException {
byte[] largeFileContentsRead = new byte[size];
InputStream largeIn = remoteLargeFile.getInputStream();
try {
readFully(largeIn, largeFileContentsRead);
} finally {
closeQuietly(largeIn);
}

assertThat(largeFileContentsRead, equalTo(largeFileContentsWritten));
return largeFileContentsRead;
}

@Test
Expand All @@ -548,15 +652,7 @@ public void shouldCopyLargeFile() throws IOException {
OverthereFile remoteLargeFile = connection.getTempFile("large.dat");
LocalFile.valueOf(largeFile).copyTo(remoteLargeFile);

byte[] largeFileContentsRead = new byte[LARGE_FILE_SIZE];
InputStream largeIn = remoteLargeFile.getInputStream();
try {
readFully(largeIn, largeFileContentsRead);
} finally {
closeQuietly(largeIn);
}

assertThat(largeFileContentsRead, equalTo(largeFileContentsWritten));
assertThat(readBytes(remoteLargeFile, LARGE_FILE_SIZE), equalTo(largeFileContentsWritten));
}

@Test
Expand Down Expand Up @@ -590,12 +686,11 @@ public void shouldCopyDirectoryWithManyFiles() throws IOException {
OverthereFile remoteLargeFolder = connection.getTempFile("small.folder");
LocalFile.valueOf(largeFolder).copyTo(remoteLargeFolder);

/* FIXME: Uncomment this code when running the itest on a local KVM host.
for(int i = 0; i < NR_OF_SMALL_FILES; i++) {
OverthereFile remoteFile = remoteLargeFolder.getFile("small" + i + ".dat");
byte[] remoteBytes = OverthereUtils.read(remoteFile);
assertThat(remoteBytes.length, equalTo(SMALL_FILE_SIZE));
}*/
}
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package com.xebialabs.overthere.spi;

import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;
import com.google.common.io.OutputSupplier;

import com.xebialabs.overthere.*;

import static com.google.common.io.ByteStreams.write;
import static com.xebialabs.overthere.ConnectionOptions.OPERATING_SYSTEM;
import static com.xebialabs.overthere.ConnectionOptions.TEMPORARY_DIRECTORY_PATH;
import static com.xebialabs.overthere.OperatingSystemFamily.getLocalHostOperatingSystemFamily;
import static com.xebialabs.overthere.local.LocalConnection.LOCAL_PROTOCOL;
import static org.mockito.Mockito.*;

public class OverthereFileLocalCopyTest {

private ConnectionOptions options;
private String protocol;
private OverthereConnection connection;

public TemporaryFolder temp = new TemporaryFolder();
private OverthereConnection otherConnection;
private ConnectionOptions otherOptions;

@BeforeMethod
public void setTypeAndOptions() throws IOException {
temp.create();
protocol = LOCAL_PROTOCOL;
options = new ConnectionOptions();
options.set(OPERATING_SYSTEM, getLocalHostOperatingSystemFamily());
options.set(TEMPORARY_DIRECTORY_PATH, temp.getRoot().getPath());
connection = Overthere.getConnection(protocol, options);
otherOptions = new ConnectionOptions();
otherOptions.set(OPERATING_SYSTEM, getLocalHostOperatingSystemFamily());
otherOptions.set(TEMPORARY_DIRECTORY_PATH, temp.newFolder("temp").getPath());
otherConnection = Overthere.getConnection(protocol, otherOptions);
}

@AfterMethod
public void cleanup() {
temp.delete();
}

@Test
public void shouldDoLocalCopyIfOverSameConnection() throws IOException {
final OverthereFile tempFile = connection.getTempFile("Foo.txt");
write(generateRandomBytes(1000), new OutputSupplier<OutputStream>() {
@Override
public OutputStream getOutput() throws IOException {
return tempFile.getOutputStream();
}
});
BaseOverthereFile spy = mock(BaseOverthereFile.class);
when(spy.getConnection()).thenReturn((BaseOverthereConnection) connection);
tempFile.copyTo(spy);
verify(spy, times(1)).localCopyFrom(tempFile);
}

@Test
public void shouldNotDoLocalCopyIfDifferentConnection() throws IOException {
final OverthereFile tempFile = connection.getTempFile("Foo.txt");
write(generateRandomBytes(1000), new OutputSupplier<OutputStream>() {
@Override
public OutputStream getOutput() throws IOException {
return tempFile.getOutputStream();
}
});
BaseOverthereFile spy = mock(BaseOverthereFile.class);
when(spy.getConnection()).thenReturn((BaseOverthereConnection) otherConnection);
tempFile.copyTo(spy);
verify(spy, times(1)).copyFrom(tempFile);
}

protected static byte[] generateRandomBytes(final int size) {
byte[] randomBytes = new byte[size];
new Random().nextBytes(randomBytes);
return randomBytes;
}

}

0 comments on commit 3f798fc

Please sign in to comment.