/*
* Copyright (C)2009 - SSHJ Contributors
*
* 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 net.schmizz.sshj.sftp;
import net.schmizz.sshj.common.StreamCopier;
import net.schmizz.sshj.sftp.Response.StatusCode;
import net.schmizz.sshj.xfer.*;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.EnumSet;
public class SFTPFileTransfer
extends AbstractFileTransfer
implements FileTransfer {
private final SFTPEngine engine;
private volatile LocalFileFilter uploadFilter;
private volatile RemoteResourceFilter downloadFilter;
private volatile boolean preserveAttributes = true;
public SFTPFileTransfer(SFTPEngine engine) {
super(engine.getLoggerFactory());
this.engine = engine;
}
public boolean getPreserveAttributes() {
return preserveAttributes;
}
public void setPreserveAttributes(boolean preserveAttributes) {
this.preserveAttributes = preserveAttributes;
}
@Override
public void upload(String source, String dest)
throws IOException {
upload(new FileSystemFile(source), dest);
}
@Override
public void download(String source, String dest)
throws IOException {
download(source, new FileSystemFile(dest));
}
@Override
public void upload(LocalSourceFile localFile, String remotePath) throws IOException {
new Uploader(localFile, remotePath).upload(getTransferListener());
}
@Override
public void download(String source, LocalDestFile dest) throws IOException {
final PathComponents pathComponents = engine.getPathHelper().getComponents(source);
final FileAttributes attributes = engine.stat(source);
new Downloader().download(getTransferListener(), new RemoteResourceInfo(pathComponents, attributes), dest);
}
public void setUploadFilter(LocalFileFilter uploadFilter) {
this.uploadFilter = uploadFilter;
}
public void setDownloadFilter(RemoteResourceFilter downloadFilter) {
this.downloadFilter = downloadFilter;
}
public LocalFileFilter getUploadFilter() {
return uploadFilter;
}
public RemoteResourceFilter getDownloadFilter() {
return downloadFilter;
}
private class Downloader {
@SuppressWarnings("PMD.MissingBreakInSwitch")
private void download(final TransferListener listener,
final RemoteResourceInfo remote,
final LocalDestFile local) throws IOException {
final LocalDestFile adjustedFile;
switch (remote.getAttributes().getType()) {
case DIRECTORY:
adjustedFile = downloadDir(listener.directory(remote.getName()), remote, local);
break;
case UNKNOWN:
log.warn("Server did not supply information about the type of file at `{}` " +
"-- assuming it is a regular file!", remote.getPath());
case REGULAR:
adjustedFile = downloadFile(listener.file(remote.getName(), remote.getAttributes().getSize()), remote, local);
break;
default:
throw new IOException(remote + " is not a regular file or directory");
}
if (getPreserveAttributes())
copyAttributes(remote, adjustedFile);
}
private LocalDestFile downloadDir(final TransferListener listener,
final RemoteResourceInfo remote,
final LocalDestFile local)
throws IOException {
final LocalDestFile adjusted = local.getTargetDirectory(remote.getName());
final RemoteDirectory rd = engine.openDir(remote.getPath());
try {
for (RemoteResourceInfo rri : rd.scan(getDownloadFilter()))
download(listener, rri, adjusted.getChild(rri.getName()));
} finally {
rd.close();
}
return adjusted;
}
private LocalDestFile downloadFile(final StreamCopier.Listener listener,
final RemoteResourceInfo remote,
final LocalDestFile local)
throws IOException {
final LocalDestFile adjusted = local.getTargetFile(remote.getName());
final RemoteFile rf = engine.open(remote.getPath());
try {
final RemoteFile.ReadAheadRemoteFileInputStream rfis = rf.new ReadAheadRemoteFileInputStream(16);
final OutputStream os = adjusted.getOutputStream();
try {
new StreamCopier(rfis, os, engine.getLoggerFactory())
.bufSize(engine.getSubsystem().getLocalMaxPacketSize())
.keepFlushing(false)
.listener(listener)
.copy();
} finally {
rfis.close();
os.close();
}
} finally {
rf.close();
}
return adjusted;
}
private void copyAttributes(final RemoteResourceInfo remote, final LocalDestFile local)
throws IOException {
final FileAttributes attrs = remote.getAttributes();
local.setPermissions(attrs.getMode().getPermissionsMask());
if (attrs.has(FileAttributes.Flag.ACMODTIME)) {
local.setLastAccessedTime(attrs.getAtime());
local.setLastModifiedTime(attrs.getMtime());
}
}
}
private class Uploader {
private final LocalSourceFile source;
private final String remote;
private Uploader(final LocalSourceFile source, final String remote) {
this.source = source;
this.remote = remote;
}
private void upload(final TransferListener listener) throws IOException {
if (source.isDirectory()) {
makeDirIfNotExists(remote); // Ensure that the directory exists
uploadDir(listener.directory(source.getName()), source, remote);
setAttributes(source, remote);
} else if (source.isFile() && isDirectory(remote)) {
String adjustedRemote = engine.getPathHelper().adjustForParent(this.remote, source.getName());
uploadFile(listener.file(source.getName(), source.getLength()), source, adjustedRemote);
setAttributes(source, adjustedRemote);
} else if (source.isFile()) {
uploadFile(listener.file(source.getName(), source.getLength()), source, remote);
setAttributes(source, remote);
} else {
throw new IOException(source + " is not a file or directory");
}
}
private void upload(final TransferListener listener,
final LocalSourceFile local,
final String remote)
throws IOException {
final String adjustedPath;
if (local.isDirectory()) {
adjustedPath = uploadDir(listener.directory(local.getName()), local, remote);
} else if (local.isFile()) {
adjustedPath = uploadFile(listener.file(local.getName(), local.getLength()), local, remote);
} else {
throw new IOException(local + " is not a file or directory");
}
setAttributes(local, adjustedPath);
}
private void setAttributes(LocalSourceFile local, String remotePath) throws IOException {
if (getPreserveAttributes()) {
engine.setAttributes(remotePath, getAttributes(local));
}
}
private String uploadDir(final TransferListener listener,
final LocalSourceFile local,
final String remote)
throws IOException {
makeDirIfNotExists(remote);
for (LocalSourceFile f : local.getChildren(getUploadFilter()))
upload(listener, f, engine.getPathHelper().adjustForParent(remote, f.getName()));
return remote;
}
private String uploadFile(final StreamCopier.Listener listener,
final LocalSourceFile local,
final String remote)
throws IOException {
final String adjusted = prepareFile(local, remote);
RemoteFile rf = null;
InputStream fis = null;
RemoteFile.RemoteFileOutputStream rfos = null;
try {
rf = engine.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC));
fis = local.getInputStream();
rfos = rf.new RemoteFileOutputStream(0, 16);
new StreamCopier(fis, rfos, engine.getLoggerFactory())
.bufSize(engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead())
.keepFlushing(false)
.listener(listener)
.copy();
} finally {
if (rf != null) {
try {
rf.close();
} catch (IOException e) {
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
}
}
if (rfos != null) {
try {
rfos.close();
} catch (IOException e) {
}
}
}
return adjusted;
}
private boolean makeDirIfNotExists(final String remote) throws IOException {
try {
FileAttributes attrs = engine.stat(remote);
if (attrs.getMode().getType() != FileMode.Type.DIRECTORY) {
throw new IOException(remote + " exists and should be a directory, but was a " + attrs.getMode().getType());
}
// Was not created, but existed.
return false;
} catch (SFTPException e) {
if (e.getStatusCode() == StatusCode.NO_SUCH_FILE) {
log.debug("makeDir: {} does not exist, creating", remote);
engine.makeDir(remote);
return true;
} else {
throw e;
}
}
}
private boolean isDirectory(final String remote) throws IOException {
try {
FileAttributes attrs = engine.stat(remote);
return attrs.getMode().getType() == FileMode.Type.DIRECTORY;
} catch (SFTPException e) {
if (e.getStatusCode() == StatusCode.NO_SUCH_FILE) {
log.debug("isDir: {} does not exist", remote);
return false;
} else {
throw e;
}
}
}
private String prepareFile(final LocalSourceFile local, final String remote)
throws IOException {
final FileAttributes attrs;
try {
attrs = engine.stat(remote);
} catch (SFTPException e) {
if (e.getStatusCode() == StatusCode.NO_SUCH_FILE) {
log.debug("probeFile: {} does not exist", remote);
return remote;
} else
throw e;
}
if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) {
throw new IOException("Trying to upload file " + local.getName() + " to path " + remote + " but that is a directory");
} else {
log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType());
return remote;
}
}
private FileAttributes getAttributes(LocalSourceFile local)
throws IOException {
final FileAttributes.Builder builder = new FileAttributes.Builder().withPermissions(local.getPermissions());
if (local.providesAtimeMtime())
builder.withAtimeMtime(local.getLastAccessTime(), local.getLastModifiedTime());
return builder.build();
}
}
}