blob: 06f387f5464a44938d4655800547a8a555b01bb0 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
package org.apache.ivy.plugins.repository.ssh;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import org.apache.ivy.core.settings.TimeoutConstraint;
import org.apache.ivy.plugins.repository.Resource;
import org.apache.ivy.util.Message;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
/**
* Ivy Repository based on SSH
*/
public class SshRepository extends AbstractSshBasedRepository {
private static final int BUFFER_SIZE = 64 * 1024;
private static final String ARGUMENT_PLACEHOLDER = "%arg";
private static final int POLL_SLEEP_TIME = 500;
private char fileSeparator = '/';
private String listCommand = "ls -1";
private String existCommand = "ls";
private String createDirCommand = "mkdir";
private String publishPermissions = null;
public SshRepository() {
}
public SshRepository(final TimeoutConstraint timeoutConstraint) {
super(timeoutConstraint);
}
/**
* create a new resource with lazy initializing
*
* @param source String
* @return Resource
*/
public Resource getResource(String source) {
Message.debug("SShRepository:getResource called: " + source);
return new SshResource(this, source);
}
/**
* Fetch the needed file information for a given file (size, last modification time) and report
* it back in a SshResource
*
* @param source
* ssh uri for the file to get info for
* @return SshResource filled with the needed information
* @see org.apache.ivy.plugins.repository.Repository#getResource(java.lang.String)
*/
public SshResource resolveResource(String source) {
Message.debug("SShRepository:resolveResource called: " + source);
SshResource result = null;
Session session = null;
try {
session = getSession(source);
Scp myCopy = new Scp(session);
Scp.FileInfo fileInfo = myCopy.getFileinfo(new URI(source).getPath());
result = new SshResource(this, source, true, fileInfo.getLength(),
fileInfo.getLastModified());
} catch (IOException | URISyntaxException e) {
if (session != null) {
releaseSession(session, source);
}
result = new SshResource();
} catch (RemoteScpException e) {
result = new SshResource();
}
Message.debug("SShRepository:resolveResource end.");
return result;
}
/**
* Reads out the output of a ssh session exec
*
* @param channel
* Channel to read from
* @param strStdout
* StringBuilder that receives Session Stdout output
* @param strStderr
* StringBuilder that receives Session Stderr output
* @throws IOException
* in case of trouble with the network
*/
private void readSessionOutput(ChannelExec channel, StringBuilder strStdout,
StringBuilder strStderr) throws IOException {
InputStream stdout = channel.getInputStream();
InputStream stderr = channel.getErrStream();
try {
channel.connect();
} catch (JSchException jsche) {
throw new IOException("Channel connection problems", jsche);
}
byte[] buffer = new byte[BUFFER_SIZE];
while (true) {
int avail = 0;
while ((avail = stdout.available()) > 0) {
int len = stdout.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
strStdout.append(new String(buffer, 0, len));
}
while ((avail = stderr.available()) > 0) {
int len = stderr.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
strStderr.append(new String(buffer, 0, len));
}
if (channel.isClosed()) {
break;
}
try {
Thread.sleep(POLL_SLEEP_TIME);
} catch (Exception ee) {
// ignored
}
}
int avail = 0;
while ((avail = stdout.available()) > 0) {
int len = stdout.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
strStdout.append(new String(buffer, 0, len));
}
while ((avail = stderr.available()) > 0) {
int len = stderr.read(buffer, 0, (avail > BUFFER_SIZE - 1 ? BUFFER_SIZE : avail));
strStderr.append(new String(buffer, 0, len));
}
}
/*
* (non-Javadoc)
*
* @see org.apache.ivy.repository.Repository#list(java.lang.String)
*/
public List<String> list(String parent) throws IOException {
Message.debug("SShRepository:list called: " + parent);
List<String> result = new ArrayList<>();
Session session = null;
ChannelExec channel = null;
session = getSession(parent);
channel = getExecChannel(session);
URI parentUri = null;
try {
parentUri = new URI(parent);
} catch (URISyntaxException e) {
throw new IOException("The uri '" + parent + "' is not valid!", e);
}
String fullCmd = replaceArgument(listCommand, parentUri.getPath());
channel.setCommand(fullCmd);
StringBuilder stdOut = new StringBuilder();
StringBuilder stdErr = new StringBuilder();
readSessionOutput(channel, stdOut, stdErr);
if (channel.getExitStatus() != 0) {
Message.error("Ssh ListCommand exited with status != 0");
Message.error(stdErr.toString());
return null;
} else {
BufferedReader br = new BufferedReader(new StringReader(stdOut.toString()));
String line = null;
while ((line = br.readLine()) != null) {
result.add(line);
}
}
return result;
}
/**
* @param session Session
* @return ChannelExec
*/
private ChannelExec getExecChannel(Session session) throws IOException {
ChannelExec channel;
try {
channel = (ChannelExec) session.openChannel("exec");
} catch (JSchException e) {
throw new IOException();
}
return channel;
}
/**
* Replace the argument placeholder with argument or append the argument if no placeholder is
* present
*
* @param command
* with argument placeholder or not
* @param argument ditto
* @return replaced full command
*/
private String replaceArgument(String command, String argument) {
String fullCmd;
if (!command.contains(ARGUMENT_PLACEHOLDER)) {
fullCmd = command + " " + argument;
} else {
fullCmd = command.replaceAll(ARGUMENT_PLACEHOLDER, argument);
}
return fullCmd;
}
/*
* (non-Javadoc)
*
* @see org.apache.ivy.repository.Repository#put(java.io.File, java.lang.String, boolean)
*/
public void put(File source, String destination, boolean overwrite) throws IOException {
Message.debug("SShRepository:put called: " + destination);
Session session = getSession(destination);
URI destinationUri = null;
try {
destinationUri = new URI(destination);
} catch (URISyntaxException e) {
throw new IOException("The uri '" + destination + "' is not valid!", e);
}
try {
String filePath = destinationUri.getPath();
int lastSep = filePath.lastIndexOf(fileSeparator);
String path;
String name;
if (lastSep == -1) {
name = filePath;
path = null;
} else {
name = filePath.substring(lastSep + 1);
path = filePath.substring(0, lastSep);
}
if (!overwrite) {
if (checkExistence(filePath, session)) {
throw new IOException("destination file exists and overwrite == false");
}
}
if (path != null) {
makePath(path, session);
}
Scp myCopy = new Scp(session);
myCopy.put(source.getCanonicalPath(), path, name, publishPermissions);
} catch (IOException e) {
if (session != null) {
releaseSession(session, destination);
}
throw e;
} catch (RemoteScpException e) {
throw new IOException(e.getMessage());
}
}
/**
* Tries to create a directory path on the target system
*
* @param path
* to create
* @param session
* to use
*/
private void makePath(String path, Session session) throws IOException {
ChannelExec channel = null;
String trimmed = path;
try {
while (trimmed.length() > 0 && trimmed.charAt(trimmed.length() - 1) == fileSeparator) {
trimmed = trimmed.substring(0, trimmed.length() - 1);
}
if (trimmed.length() == 0 || checkExistence(trimmed, session)) {
return;
}
int nextSlash = trimmed.lastIndexOf(fileSeparator);
if (nextSlash > 0) {
String parent = trimmed.substring(0, nextSlash);
makePath(parent, session);
}
channel = getExecChannel(session);
String mkdir = replaceArgument(createDirCommand, trimmed);
Message.debug("SShRepository: trying to create path: " + mkdir);
channel.setCommand(mkdir);
StringBuilder stdOut = new StringBuilder();
StringBuilder stdErr = new StringBuilder();
readSessionOutput(channel, stdOut, stdErr);
} finally {
if (channel != null) {
channel.disconnect();
}
}
}
/**
* check for existence of file or dir on target system
*
* @param filePath
* to the object to check
* @param session
* to use
* @return true: object exists, false otherwise
*/
private boolean checkExistence(String filePath, Session session) throws IOException {
Message.debug("SShRepository: checkExistence called: " + filePath);
ChannelExec channel = null;
channel = getExecChannel(session);
String fullCmd = replaceArgument(existCommand, filePath);
channel.setCommand(fullCmd);
StringBuilder stdOut = new StringBuilder();
StringBuilder stdErr = new StringBuilder();
readSessionOutput(channel, stdOut, stdErr);
return channel.getExitStatus() == 0;
}
/*
* (non-Javadoc)
*
* @see org.apache.ivy.repository.Repository#get(java.lang.String, java.io.File)
*/
public void get(String source, File destination) throws IOException {
Message.debug("SShRepository:get called: " + source + " to "
+ destination.getCanonicalPath());
if (destination.getParentFile() != null) {
destination.getParentFile().mkdirs();
}
Session session = getSession(source);
URI sourceUri = null;
try {
sourceUri = new URI(source);
} catch (URISyntaxException e) {
throw new IOException("The uri '" + source + "' is not valid!", e);
}
try {
Scp myCopy = new Scp(session);
myCopy.get(sourceUri.getPath(), destination.getCanonicalPath());
} catch (IOException e) {
if (session != null) {
releaseSession(session, source);
}
throw e;
} catch (RemoteScpException e) {
throw new IOException(e.getMessage());
}
}
/**
* sets the list command to use for a directory listing listing must be only the filename and
* each filename on a separate line
*
* @param cmd
* to use. default is "ls -1"
*/
public void setListCommand(String cmd) {
this.listCommand = cmd.trim();
}
/**
* @return the list command to use
*/
public String getListCommand() {
return listCommand;
}
/**
* @return the createDirCommand
*/
public String getCreateDirCommand() {
return createDirCommand;
}
/**
* @param createDirCommand
* the createDirCommand to set
*/
public void setCreateDirCommand(String createDirCommand) {
this.createDirCommand = createDirCommand;
}
/**
* @return the existCommand
*/
public String getExistCommand() {
return existCommand;
}
/**
* @param existCommand
* the existCommand to set
*/
public void setExistCommand(String existCommand) {
this.existCommand = existCommand;
}
/**
* The file separator is the separator to use on the target system On a unix system it is '/',
* but I don't know, how this is solved on different ssh implementations. Using the default
* might be fine
*
* @param fileSeparator
* The fileSeparator to use. default '/'
*/
public void setFileSeparator(char fileSeparator) {
this.fileSeparator = fileSeparator;
}
/**
* A four digit string (e.g., 0644, see "man chmod", "man open") specifying the permissions of
* the published files.
*
* @param permissions String
*/
public void setPublishPermissions(String permissions) {
this.publishPermissions = permissions;
}
/**
* return ssh as scheme use the Resolver type name here? would be nice if it would be static, so
* we could use SshResolver.getTypeName()
*
* @return String
*/
protected String getRepositoryScheme() {
return "ssh";
}
/**
* Not really streaming...need to implement a proper streaming approach?
*
* @param resource
* to stream
* @return InputStream of the resource data
* @throws IOException if something goes wrong
*/
public InputStream openStream(SshResource resource) throws IOException {
Session session = getSession(resource.getName());
Scp scp = new Scp(session);
ByteArrayOutputStream os = new ByteArrayOutputStream();
try {
scp.get(resource.getName(), os);
} catch (IOException e) {
if (session != null) {
releaseSession(session, resource.getName());
}
throw e;
} catch (RemoteScpException e) {
throw new IOException(e.getMessage());
}
return new ByteArrayInputStream(os.toByteArray());
}
}