/* * Copyright 2010 netling project <http://netling.org> * * 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.netling.sftp; import java.io.File; import java.io.FileFilter; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.util.EnumSet; import org.netling.io.StreamCopier; import org.netling.sftp.Response.StatusCode; import org.netling.xfer.AbstractFileTransfer; import org.netling.xfer.FileTransfer; import org.netling.xfer.FileTransferUtil; import org.netling.xfer.TransferListener; public class SFTPFileTransfer extends AbstractFileTransfer implements FileTransfer { private final SFTPEngine engine; private final PathHelper pathHelper; private volatile FileFilter uploadFilter = defaultLocalFilter; private volatile RemoteResourceFilter downloadFilter = defaultRemoteFilter; private static final FileFilter defaultLocalFilter = new FileFilter() { @Override public boolean accept(File pathName) { return true; } }; private static final RemoteResourceFilter defaultRemoteFilter = new RemoteResourceFilter() { @Override public boolean accept(RemoteResourceInfo resource) { return true; } }; public SFTPFileTransfer(SFTPEngine engine) { this.engine = engine; this.pathHelper = new PathHelper(engine); } @Override public void upload(String source, String dest) throws IOException { new Uploader().upload(new File(source), dest); } @Override public void download(String source, String dest) throws IOException { final PathComponents pathComponents = pathHelper.getComponents(source); final FileAttributes attributes = engine.stat(source); new Downloader().download(new RemoteResourceInfo(pathComponents, attributes), new File(dest)); } public void setUploadFilter(FileFilter uploadFilter) { this.uploadFilter = (this.uploadFilter == null) ? defaultLocalFilter : uploadFilter; } public void setDownloadFilter(RemoteResourceFilter downloadFilter) { this.downloadFilter = (this.downloadFilter == null) ? defaultRemoteFilter : downloadFilter; } public FileFilter getUploadFilter() { return uploadFilter; } public RemoteResourceFilter getDownloadFilter() { return downloadFilter; } private class Downloader { private final TransferListener listener = getTransferListener(); private void download(final RemoteResourceInfo remote, final File local) throws IOException { final File adjustedFile; switch (remote.getAttributes().getType()) { case DIRECTORY: listener.startedDir(remote.getName()); adjustedFile = downloadDir(remote, local); listener.finishedDir(); 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: listener.startedFile(remote.getName(), remote.getAttributes().getSize()); adjustedFile = downloadFile(remote, local); listener.finishedFile(); break; default: throw new IOException(remote + " is not a regular file or directory"); } copyAttributes(remote, adjustedFile); } private File downloadDir(final RemoteResourceInfo remote, final File local) throws IOException { final File adjusted = FileTransferUtil.getTargetDirectory(local, remote.getName()); final RemoteDirectory rd = engine.openDir(remote.getPath()); try { for (RemoteResourceInfo rri : rd.scan(getDownloadFilter())) download(rri, new File(adjusted.getPath(), rri.getName())); } finally { rd.close(); } return adjusted; } private File downloadFile(final RemoteResourceInfo remote, final File local) throws IOException { final File adjusted = FileTransferUtil.getTargetFile(local, remote.getName()); final RemoteFile rf = engine.open(remote.getPath()); try { final FileOutputStream fos = new FileOutputStream(adjusted); try { StreamCopier.copy(rf.getInputStream(), fos, engine.getSubsystem() .getLocalMaxPacketSize(), false, listener); } finally { fos.close(); } } finally { rf.close(); } return adjusted; } private void copyAttributes(final RemoteResourceInfo remote, final File local) throws IOException { final FileAttributes attrs = remote.getAttributes(); getModeSetter().setPermissions(local, attrs.getMode().getPermissionsMask()); if (getModeSetter().preservesTimes() && attrs.has(FileAttributes.Flag.ACMODTIME)) { getModeSetter().setLastAccessedTime(local, attrs.getAtime()); getModeSetter().setLastModifiedTime(local, attrs.getMtime()); } } } private class Uploader { private final TransferListener listener = getTransferListener(); private void upload(File local, String remote) throws IOException { final String adjustedPath; if (local.isDirectory()) { listener.startedDir(local.getName()); adjustedPath = uploadDir(local, remote); listener.finishedDir(); } else if (local.isFile()) { listener.startedFile(local.getName(), local.length()); adjustedPath = uploadFile(local, remote); listener.finishedFile(); } else throw new IOException(local + " is not a file or directory"); engine.setAttributes(adjustedPath, getAttributes(local)); } private String uploadDir(File local, String remote) throws IOException { final String adjusted = prepareDir(local, remote); for (File f : local.listFiles(getUploadFilter())) upload(f, adjusted); return adjusted; } private String uploadFile(File local, String remote) throws IOException { final String adjusted = prepareFile(local, remote); final RemoteFile rf = engine.open(adjusted, EnumSet.of(OpenMode.WRITE, OpenMode.CREAT, OpenMode.TRUNC)); try { final FileInputStream fis = new FileInputStream(local); try { final int bufSize = engine.getSubsystem().getRemoteMaxPacketSize() - rf.getOutgoingPacketOverhead(); StreamCopier.copy(fis, rf.getOutputStream(), bufSize, false, listener); } finally { fis.close(); } } finally { rf.close(); } return adjusted; } private String prepareDir(File local, String remote) throws IOException { final FileAttributes attrs; try { attrs = engine.stat(remote); } catch (SFTPException e) { if (e.getStatusCode() == StatusCode.NO_SUCH_FILE) { log.debug("probeDir: {} does not exist, creating", remote); engine.makeDir(remote); return remote; } else throw e; } if (attrs.getMode().getType() == FileMode.Type.DIRECTORY) if (pathHelper.getComponents(remote).getName().equals(local.getName())) { log.debug("probeDir: {} already exists", remote); return remote; } else { log.debug("probeDir: {} already exists, path adjusted for {}", remote, local.getName()); return prepareDir(local, PathComponents.adjustForParent(remote, local.getName())); } else throw new IOException(attrs.getMode().getType() + " file already exists at " + remote); } private String prepareFile(File local, 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) { log.debug("probeFile: {} was directory, path adjusted for {}", remote, local.getName()); remote = PathComponents.adjustForParent(remote, local.getName()); return remote; } else { log.debug("probeFile: {} is a {} file that will be replaced", remote, attrs.getMode().getType()); return remote; } } private FileAttributes getAttributes(File local) throws IOException { final FileAttributes.Builder builder = new FileAttributes.Builder() .withPermissions(getModeGetter().getPermissions(local)); if (getModeGetter().preservesTimes()) builder.withAtimeMtime(getModeGetter().getLastAccessTime(local), getModeGetter().getLastModifiedTime(local)); return builder.build(); } } }