/*
* Copyright 2002-2016 the original author or authors.
*
* Licensed 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
*
* http://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.springframework.integration.sftp.session;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.NestedIOException;
import org.springframework.integration.file.remote.session.Session;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
/**
* Default SFTP {@link Session} implementation. Wraps a JSCH session instance.
*
* @author Josh Long
* @author Mario Gray
* @author Mark Fisher
* @author Oleg Zhurakousky
* @author Gary Russell
* @since 2.0
*/
public class SftpSession implements Session<LsEntry> {
private final Log logger = LogFactory.getLog(this.getClass());
private final com.jcraft.jsch.Session jschSession;
private final JSchSessionWrapper wrapper;
private volatile ChannelSftp channel;
private volatile boolean closed;
public SftpSession(com.jcraft.jsch.Session jschSession) {
Assert.notNull(jschSession, "jschSession must not be null");
this.jschSession = jschSession;
this.wrapper = null;
}
public SftpSession(JSchSessionWrapper wrapper) {
Assert.notNull(wrapper, "wrapper must not be null");
this.jschSession = wrapper.getSession();
this.wrapper = wrapper;
}
@Override
public boolean remove(String path) throws IOException {
Assert.state(this.channel != null, "session is not connected");
try {
this.channel.rm(path);
return true;
}
catch (SftpException e) {
throw new NestedIOException("Failed to remove file.", e);
}
}
@Override
public LsEntry[] list(String path) throws IOException {
Assert.state(this.channel != null, "session is not connected");
try {
Vector<?> lsEntries = this.channel.ls(path);
if (lsEntries != null) {
LsEntry[] entries = new LsEntry[lsEntries.size()];
for (int i = 0; i < lsEntries.size(); i++) {
Object next = lsEntries.get(i);
Assert.state(next instanceof LsEntry, "expected only LsEntry instances from channel.ls()");
entries[i] = (LsEntry) next;
}
return entries;
}
}
catch (SftpException e) {
throw new NestedIOException("Failed to list files", e);
}
return new LsEntry[0];
}
@Override
public String[] listNames(String path) throws IOException {
LsEntry[] entries = this.list(path);
List<String> names = new ArrayList<String>();
for (int i = 0; i < entries.length; i++) {
String fileName = entries[i].getFilename();
SftpATTRS attrs = entries[i].getAttrs();
if (!attrs.isDir() && !attrs.isLink()) {
names.add(fileName);
}
}
String[] fileNames = new String[names.size()];
return names.toArray(fileNames);
}
@Override
public void read(String source, OutputStream os) throws IOException {
Assert.state(this.channel != null, "session is not connected");
try {
InputStream is = this.channel.get(source);
FileCopyUtils.copy(is, os);
}
catch (SftpException e) {
throw new NestedIOException("failed to read file " + source, e);
}
}
@Override
public InputStream readRaw(String source) throws IOException {
try {
return this.channel.get(source);
}
catch (SftpException e) {
throw new NestedIOException("failed to read file " + source, e);
}
}
@Override
public boolean finalizeRaw() throws IOException {
return true;
}
@Override
public void write(InputStream inputStream, String destination) throws IOException {
Assert.state(this.channel != null, "session is not connected");
try {
this.channel.put(inputStream, destination);
}
catch (SftpException e) {
throw new NestedIOException("failed to write file", e);
}
}
@Override
public void append(InputStream inputStream, String destination) throws IOException {
Assert.state(this.channel != null, "session is not connected");
try {
this.channel.put(inputStream, destination, ChannelSftp.APPEND);
}
catch (SftpException e) {
throw new NestedIOException("failed to write file", e);
}
}
@Override
public void close() {
this.closed = true;
if (this.wrapper != null) {
if (this.channel != null) {
this.channel.disconnect();
}
this.wrapper.close();
}
else {
if (this.jschSession.isConnected()) {
this.jschSession.disconnect();
}
}
}
@Override
public boolean isOpen() {
return !this.closed && this.jschSession.isConnected();
}
@Override
public void rename(String pathFrom, String pathTo) throws IOException {
try {
this.channel.rename(pathFrom, pathTo);
}
catch (SftpException sftpex) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Initial File rename failed, possibly because file already exists. Will attempt to delete file: "
+ pathTo + " and execute rename again.");
}
try {
this.remove(pathTo);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Delete file: " + pathTo + " succeeded. Will attempt rename again");
}
}
catch (IOException ioex) {
throw new NestedIOException("Failed to delete file " + pathTo, ioex);
}
try {
// attempt to rename again
this.channel.rename(pathFrom, pathTo);
}
catch (SftpException sftpex2) {
throw new NestedIOException("failed to rename from " + pathFrom + " to " + pathTo, sftpex2);
}
}
if (this.logger.isDebugEnabled()) {
this.logger.debug("File: " + pathFrom + " was successfully renamed to " + pathTo);
}
}
@Override
public boolean mkdir(String remoteDirectory) throws IOException {
try {
this.channel.mkdir(remoteDirectory);
}
catch (SftpException e) {
throw new NestedIOException("failed to create remote directory '" + remoteDirectory + "'.", e);
}
return true;
}
@Override
public boolean rmdir(String remoteDirectory) throws IOException {
try {
this.channel.rmdir(remoteDirectory);
}
catch (SftpException e) {
throw new NestedIOException("failed to remove remote directory '" + remoteDirectory + "'.", e);
}
return true;
}
@Override
public boolean exists(String path) {
try {
this.channel.lstat(path);
return true;
}
catch (SftpException e) {
// ignore
}
return false;
}
void connect() {
try {
if (!this.jschSession.isConnected()) {
this.jschSession.connect();
}
this.channel = (ChannelSftp) this.jschSession.openChannel("sftp");
if (this.channel != null && !this.channel.isConnected()) {
this.channel.connect();
}
}
catch (JSchException e) {
this.close();
throw new IllegalStateException("failed to connect", e);
}
}
@Override
public ChannelSftp getClientInstance() {
return this.channel;
}
}