/*
* Copyright (c) 2008-2014, XebiaLabs B.V., All rights reserved.
*
*
* Overthere is licensed under the terms of the GPLv2
* <http://www.gnu.org/licenses/old-licenses/gpl-2.0.html>, like most XebiaLabs Libraries.
* There are special exceptions to the terms and conditions of the GPLv2 as it is applied to
* this software, see the FLOSS License Exception
* <http://github.com/xebialabs/overthere/blob/master/LICENSE>.
*
* 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; version 2
* of the License.
*
* 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, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth
* Floor, Boston, MA 02110-1301 USA
*/
package com.xebialabs.overthere.ssh;
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.List;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.xebialabs.overthere.CmdLine;
import com.xebialabs.overthere.OverthereFile;
import com.xebialabs.overthere.RuntimeIOException;
import com.xebialabs.overthere.util.CapturingOverthereExecutionOutputHandler;
import net.schmizz.sshj.xfer.LocalFileFilter;
import net.schmizz.sshj.xfer.LocalSourceFile;
import net.schmizz.sshj.xfer.scp.SCPUploadClient;
import static com.xebialabs.overthere.CmdLine.build;
import static com.xebialabs.overthere.ssh.SshConnection.NOCD_PSEUDO_COMMAND;
import static com.xebialabs.overthere.util.CapturingOverthereExecutionOutputHandler.capturingHandler;
import static com.xebialabs.overthere.util.LoggingOverthereExecutionOutputHandler.loggingErrorHandler;
import static com.xebialabs.overthere.util.LoggingOverthereExecutionOutputHandler.loggingOutputHandler;
import static com.xebialabs.overthere.util.MultipleOverthereExecutionOutputHandler.multiHandler;
import static com.xebialabs.overthere.util.NullOverthereExecutionOutputHandler.swallow;
import static java.lang.String.format;
/**
* A file on a host connected through SSH w/ SCP.
*/
class SshScpFile extends SshFile<SshScpConnection> {
private static final String PERMISSIONS_TOKEN_PATTERN = "[dl\\-]([r\\-][w\\-][xsStT\\-]){3}[@\\.\\+]*";
private static Pattern permissionsTokenPattern = Pattern.compile(PERMISSIONS_TOKEN_PATTERN);
/**
* Constructs an SshScpOverthereFile
*
* @param connection the connection to the host
* @param remotePath the path of the file on the host
*/
public SshScpFile(SshScpConnection connection, String remotePath) {
super(connection, remotePath);
}
@Override
public boolean exists() {
return getFileInfo().exists;
}
@Override
public boolean canRead() {
return getFileInfo().canRead;
}
@Override
public boolean canWrite() {
return getFileInfo().canWrite;
}
@Override
public boolean canExecute() {
return getFileInfo().canExecute;
}
@Override
public boolean isFile() {
return getFileInfo().isFile;
}
@Override
public boolean isDirectory() {
return getFileInfo().isDirectory;
}
@Override
public long lastModified() {
// FIXME: Implement by parsing the date output of `ls -l`
throw new UnsupportedOperationException();
}
@Override
public long length() {
return getFileInfo().length;
}
/**
* Gets information about the file by executing "ls -ld" on it.
*
* @return the information about the file, never <code>null</code>.
* @throws RuntimeIOException if an I/O exception occurs
*/
public LsResults getFileInfo() throws RuntimeIOException {
logger.debug("Retrieving file info of {}", this);
CmdLine lsCmdLine = CmdLine.build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(connection.getFileInfoCommand, getPath());
LsResults results = new LsResults();
CapturingOverthereExecutionOutputHandler capturedOutput = capturingHandler();
int errno = executeCommand(capturedOutput, swallow(), lsCmdLine);
if (errno == 0) {
for (int i = capturedOutput.getOutputLines().size() - 1; i >= 0; i--) {
if (parseLsOutputLine(results, capturedOutput.getOutputLines().get(i))) {
results.exists = true;
break;
}
}
if (!results.exists) {
throw new RuntimeIOException("ls -ld " + getPath() + " returned " + errno + " but its output is unparseable: " + capturedOutput.getOutput());
}
} else {
results.exists = false;
}
logger.debug("Listed file {}: exists={}, isDirectory={}, length={}, canRead={}, canWrite={}, canExecute={}", new Object[]{this, results.exists,
results.isDirectory, results.length
, results.canRead, results.canWrite, results.canExecute});
return results;
}
protected boolean parseLsOutputLine(LsResults results, String outputLine) {
StringTokenizer outputTokens = new StringTokenizer(outputLine);
if (outputTokens.countTokens() < 5) {
logger.debug("Not parsing ls output line [{}] because it has less than 5 tokens", outputLine);
return false;
}
String permissions = outputTokens.nextToken();
if (!permissionsTokenPattern.matcher(permissions).matches()) {
logger.debug("Not parsing ls output line [{}] because it the first token does not match the pattern for permissions [" + PERMISSIONS_TOKEN_PATTERN
+ "]", outputLine);
return false;
}
logger.debug("Parsing ls output line [{}]", outputLine);
outputTokens.nextToken(); // inodelinks
outputTokens.nextToken(); // owner
outputTokens.nextToken(); // group
String size = outputTokens.nextToken();
results.isFile = permissions.length() >= 1 && permissions.charAt(0) == '-';
results.isDirectory = permissions.length() >= 1 && permissions.charAt(0) == 'd';
results.canRead = permissions.length() >= 2 && permissions.charAt(1) == 'r';
results.canWrite = permissions.length() >= 3 && permissions.charAt(2) == 'w';
results.canExecute = permissions.length() >= 4 && (permissions.charAt(3) == 'x' || permissions.charAt(3) == 's' || permissions.charAt(3) == 't');
try {
results.length = Integer.parseInt(size);
} catch (NumberFormatException exc) {
logger.warn("Cannot parse length of " + this.getPath() + " from ls output: " + outputLine + ". Length will be reported as -1.", exc);
}
return true;
}
/**
* Holds results of an ls call
*/
public static class LsResults {
public boolean exists;
public boolean isFile;
public boolean isDirectory;
public long length = -1;
public boolean canRead;
public boolean canWrite;
public boolean canExecute;
}
@Override
public InputStream getInputStream() throws RuntimeIOException {
try {
final File tempFile = File.createTempFile("scp_download", ".tmp");
tempFile.deleteOnExit();
logger.debug("Downloading contents of {} to temporary file {}", this, tempFile);
connection.getSshClient().newSCPFileTransfer().download(getPath(), tempFile.getPath());
logger.debug("Opening input stream to temporary file {} to retrieve contents downloaded from {}. Temporary file will be deleted when the stream is closed", tempFile, this);
return asBuffered(new FileInputStream(tempFile) {
@Override
public void close() throws IOException {
logger.debug("Closing input stream to temporary file {}", tempFile);
try {
super.close();
} finally {
logger.debug("Deleting temporary file {}", tempFile);
tempFile.delete();
}
}
});
} catch (IOException exc) {
throw new RuntimeIOException(format("Cannot open %s for reading: %s", this, exc.toString()), exc);
}
}
@Override
public OutputStream getOutputStream() throws RuntimeIOException {
try {
final File tempFile = File.createTempFile("scp_upload", ".tmp");
tempFile.deleteOnExit();
logger.debug("Opening output stream to temporary file {} to store contents to be uploaded to {} when the stream is closed", tempFile, SshScpFile.this);
return asBuffered(new FileOutputStream(tempFile) {
@Override
public void close() throws IOException {
logger.debug("Closing output stream to temporary file {}", tempFile);
try {
super.close();
} finally {
uploadAndDelete(tempFile);
}
}
private void uploadAndDelete(File tempFile) throws IOException {
logger.debug("Uploading contents of temporary file {} to to {}", tempFile, SshScpFile.this);
try {
connection.getSshClient().newSCPFileTransfer().upload(tempFile.getPath(), getPath());
} finally {
logger.debug("Deleting temporary file {}", tempFile);
tempFile.delete();
}
}
});
} catch (IOException exc) {
throw new RuntimeIOException(format("Cannot open %s for writing: %s", this, exc.toString()), exc);
}
}
@Override
public List<OverthereFile> listFiles() {
logger.debug("Listing directory {}", this);
CmdLine lsCmdLine = build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(connection.listFilesCommand, getPath());
CapturingOverthereExecutionOutputHandler capturedStdout = capturingHandler();
CapturingOverthereExecutionOutputHandler capturedStderr = capturingHandler();
int errno = executeCommand(multiHandler(loggingOutputHandler(logger), capturedStdout), multiHandler(loggingErrorHandler(logger), capturedStderr), lsCmdLine);
if (errno != 0) {
throw new RuntimeIOException("Cannot list directory " + this + ": " + capturedStderr.getOutput() + " (errno=" + errno + ")");
}
List<OverthereFile> files = new ArrayList<OverthereFile>();
for (String lsLine : capturedStdout.getOutputLines()) {
// Filter out the '.' and '..'
if (!(".".equals(lsLine) || "..".equals(lsLine))) {
files.add(connection.getFile(this, lsLine));
}
}
return files;
}
@Override
public void mkdir() {
logger.debug("Creating directory {}", this);
mkdir(connection.mkdirCommand);
}
@Override
public void mkdirs() {
logger.debug("Creating directories {}", this);
mkdir(connection.mkdirsCommand);
}
protected void mkdir(String command) throws RuntimeIOException {
CmdLine mkdirCmdLine = CmdLine.build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(command, getPath());
executeAndThrowOnErrorCode(mkdirCmdLine, "Cannot create directory or -ies " + this);
if (logger.isDebugEnabled()) {
logger.debug("Created directory " + this + " (using command: " + command + ")");
}
}
@Override
public void renameTo(OverthereFile dest) {
logger.debug("Renaming {} to {}", this, dest);
if (dest instanceof SshScpFile) {
SshScpFile sshScpDestFile = (SshScpFile) dest;
if (sshScpDestFile.getConnection() == getConnection()) {
CmdLine mvCmdLine = CmdLine.build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(connection.renameToCommand, getPath(), sshScpDestFile.getPath());
executeAndThrowOnErrorCode(mvCmdLine, "Cannot rename file/directory " + this);
} else {
throw new RuntimeIOException("Cannot rename :ssh:" + connection.sshConnectionType.toString().toLowerCase() + ": file/directory " + this
+ " to file/directory "
+ dest + " because it is in a different connection");
}
} else {
throw new RuntimeIOException("Cannot rename :ssh:" + connection.sshConnectionType.toString().toLowerCase() + ": file/directory " + this
+ " to non-:ssh:"
+ connection.sshConnectionType.toString().toLowerCase() + ": file/directory " + dest);
}
}
@Override
public void setExecutable(boolean executable) {
logger.debug("Setting execute permission on {} to {}", this, executable);
CmdLine chmodCmdLine = CmdLine.build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(executable ? connection.setExecutableCommand : connection.setNotExecutableCommand, getPath());
executeAndThrowOnErrorCode(chmodCmdLine, "Cannot set execute permission on file " + this + " to " + executable);
}
@Override
protected void deleteDirectory() {
logger.debug("Deleting directory {}", this);
CmdLine rmdirCmdLine = CmdLine.build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(connection.deleteDirectoryCommand, getPath());
executeAndThrowOnErrorCode(rmdirCmdLine, "Cannot delete directory " + this);
}
@Override
protected void deleteFile() {
logger.debug("Deleting file {}", this);
CmdLine rmCmdLine = CmdLine.build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(connection.deleteFileCommand, getPath());
executeAndThrowOnErrorCode(rmCmdLine, "Cannot delete file " + this);
}
@Override
public void deleteRecursively() throws RuntimeIOException {
logger.debug("Recursively deleting file or directory {}", this);
CmdLine rmCmdLine = CmdLine.build(NOCD_PSEUDO_COMMAND).addTemplatedFragment(connection.deleteRecursivelyCommand, getPath());
executeAndThrowOnErrorCode(rmCmdLine, "Cannot recursively delete file or directory " + this);
}
@Override
protected void copyFrom(OverthereFile source) {
logger.debug("Copying file or directory {} to {}", source, this);
SCPUploadClient uploadClient = connection.getSshClient().newSCPFileTransfer().newSCPUploadClient();
try {
if (source.isDirectory() && this.exists()) {
for (OverthereFile sourceFile : source.listFiles()) {
uploadClient.copy(new OverthereFileLocalSourceFile(sourceFile), getPath());
}
} else {
uploadClient.copy(new OverthereFileLocalSourceFile(source), getPath());
}
} catch (IOException e) {
throw new RuntimeIOException("Cannot copy " + source + " to " + this + ": " + e.toString(), e);
}
}
private void executeAndThrowOnErrorCode(CmdLine mkdirCmdLine, String message) {
CapturingOverthereExecutionOutputHandler capturedStderr = capturingHandler();
int errno = executeCommand(loggingOutputHandler(logger), multiHandler(loggingErrorHandler(logger), capturedStderr), mkdirCmdLine);
if (errno != 0) {
throw new RuntimeIOException(format("%s: %s (errno=%d)", message, capturedStderr.getOutput(), errno));
}
}
protected static class OverthereFileLocalSourceFile implements LocalSourceFile {
private OverthereFile f;
public OverthereFileLocalSourceFile(OverthereFile f) {
this.f = f;
}
@Override
public String getName() {
return f.getName();
}
@Override
public long getLength() {
return f.length();
}
@Override
public InputStream getInputStream() throws IOException {
return f.getInputStream();
}
@Override
public int getPermissions() throws IOException {
return f.isDirectory() ? 0755 : 0644;
}
@Override
public boolean isFile() {
return f.isFile();
}
@Override
public boolean isDirectory() {
return f.isDirectory();
}
@Override
public Iterable<? extends LocalSourceFile> getChildren(LocalFileFilter filter) throws IOException {
List<LocalSourceFile> files = new ArrayList<LocalSourceFile>();
for (OverthereFile each : f.listFiles()) {
files.add(new OverthereFileLocalSourceFile(each));
}
return files;
}
@Override
public boolean providesAtimeMtime() {
return false;
}
@Override
public long getLastAccessTime() throws IOException {
return 0;
}
@Override
public long getLastModifiedTime() throws IOException {
return 0;
}
}
private static Logger logger = LoggerFactory.getLogger(SshScpFile.class);
}