/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (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.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is part of dcm4che, an implementation of DICOM(TM) in
* Java(TM), hosted at https://github.com/gunterze/dcm4che.
*
* The Initial Developer of the Original Code is
* Agfa Healthcare.
* Portions created by the Initial Developer are Copyright (C) 2012-2015
* the Initial Developer. All Rights Reserved.
*
* Contributor(s):
* See @authors listed below
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ***** */
package org.dcm4chee.storage.sftp;
import java.io.FilterInputStream;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Vector;
import javax.enterprise.context.Dependent;
import javax.inject.Named;
import org.dcm4che3.util.StreamUtils;
import org.dcm4chee.storage.ObjectAlreadyExistsException;
import org.dcm4chee.storage.ObjectNotFoundException;
import org.dcm4chee.storage.RetrieveContext;
import org.dcm4chee.storage.StorageContext;
import org.dcm4chee.storage.conf.StorageSystem;
import org.dcm4chee.storage.spi.StorageSystemProvider;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.ChannelSftp.LsEntry;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpATTRS;
import com.jcraft.jsch.SftpException;
import com.jcraft.jsch.SftpStatVFS;
/**
* @author Steve Kroetsch<stevekroetsch@hotmail.com>
*
*/
@Named("org.dcm4chee.storage.sftp")
@Dependent
public class SftpStorageSystemProvider implements StorageSystemProvider {
private static int DEFAULT_PORT = 22;
private Session session;
private StorageSystem storageSystem;
@Override
public void init(StorageSystem storageSystem) {
this.storageSystem = storageSystem;
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if (session != null)
session.disconnect();
}
});
}
private synchronized ChannelSftp openChannel() throws IOException {
if (session == null || !session.isConnected()) {
JSch jsch = new JSch();
String host = storageSystem.getStorageSystemHostname();
int port = storageSystem.getStorageSystemPort();
if (port == -1)
port = DEFAULT_PORT;
String user = storageSystem.getStorageSystemIdentity();
try {
session = jsch.getSession(user, host, port);
} catch (JSchException e) {
throw new IOException("Session create failed", e);
}
session.setPassword(storageSystem.getStorageSystemCredential());
Properties config = new Properties();
config.put("StrictHostKeyChecking", "no");
session.setConfig(config);
try {
session.connect();
} catch (JSchException e) {
throw new IOException("Session connect failed for server " + host + ":"
+ port + " and " + " user " + user, e);
}
}
ChannelSftp channel;
try {
channel = (ChannelSftp) session.openChannel("sftp");
} catch (JSchException e) {
throw new IOException("Open channel failed", e);
}
try {
channel.connect();
} catch (JSchException e) {
throw new IOException("Connect channel failed", e);
}
return channel;
}
@Override
public void checkWriteable() throws IOException {
}
@Override
public long getUsableSpace() throws IOException {
ChannelSftp channel = openChannel();
try {
SftpStatVFS stat = channel.statVFS(storageSystem.getStorageSystemPath());
return stat.getAvailForNonRoot();
} catch (SftpException e) {
throw new IOException("Usable space check failed for path "
+ storageSystem.getStorageSystemPath(), e);
} finally {
channel.disconnect();
}
}
@Override
public long getTotalSpace() throws IOException {
ChannelSftp channel = openChannel();
try {
SftpStatVFS stat = channel.statVFS(storageSystem.getStorageSystemPath());
return stat.getCapacity();
} catch (SftpException e) {
throw new IOException("Total space check failed for path "
+ storageSystem.getStorageSystemPath(), e);
} finally {
channel.disconnect();
}
}
@Override
public OutputStream openOutputStream(final StorageContext context, final String name)
throws IOException {
final ChannelSftp channel = openChannel();
final String dest = resolvePath(name);
try {
if (exists(channel, dest))
throw new ObjectAlreadyExistsException(
storageSystem.getStorageSystemPath(), name);
String dir = getParentDir(dest);
if (!exists(channel, dir))
mkdirs(channel, dir);
return new FilterOutputStream(channel.put(dest)) {
@Override
public void close() throws IOException {
super.close();
try {
SftpATTRS attrs = channel.stat(dest);
context.setFileSize(attrs.getSize());
} catch (SftpException e) {
throw new IOException("Get file size failed for path " + dest, e);
} finally {
channel.disconnect();
}
}
};
} catch (SftpException e) {
throw new IOException("Open output stream failed for path " + dest, e);
}
}
private boolean exists(ChannelSftp channel, String path) throws SftpException {
try {
channel.stat(path);
return true;
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
return false;
throw e;
}
}
private void mkdirs(ChannelSftp channel, String dir) throws SftpException {
String parent = getParentDir(dir);
if (parent == null)
return;
if (!exists(channel, parent))
mkdirs(channel, parent);
channel.mkdir(dir);
}
private String getParentDir(String path) {
int pos = path.lastIndexOf('/');
if (pos == -1)
return null;
String dir = path.substring(0, pos);
return dir.isEmpty() ? null : dir;
}
private String resolvePath(String name) {
StringBuilder sb = new StringBuilder(storageSystem.getStorageSystemPath());
if (sb.charAt(sb.length() - 1) != '/')
sb.append('/');
sb.append(name);
return sb.toString();
}
@Override
public void copyInputStream(final StorageContext context, InputStream in, String name)
throws IOException {
try (OutputStream out = openOutputStream(context, name)) {
StreamUtils.copy(in, out);
}
}
@Override
public void storeFile(final StorageContext context, Path path, String name)
throws IOException {
try (OutputStream out = openOutputStream(context, name)) {
Files.copy(path, out);
}
}
@Override
public void moveFile(StorageContext context, Path path, String name)
throws IOException {
try (OutputStream out = openOutputStream(context, name)) {
Files.copy(path, out);
}
Files.delete(path);
}
@Override
public InputStream openInputStream(RetrieveContext ctx, String name)
throws IOException {
String src = resolvePath(name);
final ChannelSftp channel = openChannel();
try {
if (!exists(channel, src))
throw new ObjectNotFoundException(storageSystem.getStorageSystemPath(),
name);
return new FilterInputStream(channel.get(src)) {
@Override
public void close() throws IOException {
super.close();
channel.disconnect();
}
};
} catch (SftpException e) {
throw new IOException("Open input stream failed for path " + src, e);
}
}
@Override
public Path getFile(RetrieveContext context, String name) throws IOException {
throw new UnsupportedOperationException();
}
@Override
public void deleteObject(StorageContext context, String name) throws IOException {
String path = resolvePath(name);
ChannelSftp channel = openChannel();
try {
try {
channel.rm(path);
} catch (SftpException e) {
if (e.id == ChannelSftp.SSH_FX_NO_SUCH_FILE)
throw new ObjectNotFoundException(
storageSystem.getStorageSystemPath(), name);
throw new IOException("Delete file failed for path " + path, e);
}
String dir = getParentDir(path);
try {
String basePath = storageSystem.getStorageSystemPath();
while (!basePath.equals(dir)) {
@SuppressWarnings("unchecked")
Vector<LsEntry> v = channel.ls(dir);
if (v.size() > 2)
break;
channel.rmdir(dir);
dir = getParentDir(dir);
}
} catch (SftpException e) {
if (e.id != ChannelSftp.SSH_FX_FAILURE)
throw new IOException("Remove directory failed for path " + dir, e);
}
} finally {
channel.disconnect();
}
}
@Override
public Path getBaseDirectory(StorageSystem system) {
throw new UnsupportedOperationException();
}
@Override
public <E extends Enum<E>> E queryStatus(RetrieveContext ctx, String name,
Class<E> enumType) throws IOException {
Map<String, String> statusFileExtensions = ctx.getStorageSystem()
.getStatusFileExtensions();
for (String ext : statusFileExtensions.keySet()) {
String path = resolvePath(name) + ext;
ChannelSftp channel = openChannel();
try {
if (exists(channel, path))
return Enum.valueOf(enumType, statusFileExtensions.get(ext));
} catch (SftpException e) {
throw new IOException("Exists check failed for path " + path, e);
} finally {
channel.disconnect();
}
}
return null;
}
@Override
public void sync (List<String> names) throws IOException {
//do nothing.
}
}