content: /*
   *  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.sftp;
  
  import java.io.File;
  import java.io.IOException;
  import java.io.InputStream;
  import java.net.URI;
  import java.net.URISyntaxException;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.List;
  
  import org.apache.ivy.core.settings.TimeoutConstraint;
  import org.apache.ivy.plugins.repository.BasicResource;
  import org.apache.ivy.plugins.repository.Resource;
  import org.apache.ivy.plugins.repository.TransferEvent;
  import org.apache.ivy.plugins.repository.ssh.AbstractSshBasedRepository;
  import org.apache.ivy.plugins.repository.ssh.SshCache;
  import org.apache.ivy.util.Message;
  
  import com.jcraft.jsch.ChannelSftp;
  import com.jcraft.jsch.ChannelSftp.LsEntry;
  import com.jcraft.jsch.JSchException;
  import com.jcraft.jsch.Session;
  import com.jcraft.jsch.SftpATTRS;
  import com.jcraft.jsch.SftpException;
  import com.jcraft.jsch.SftpProgressMonitor;
  
  /**
   * SFTP Repository, allow to use a repository accessed by sftp protocol. It supports all operations:
   * get, put and list. It relies on jsch for sftp handling, and thus is compatible with sftp version
   * 0, 1, 2 and 3
   */
  public class SFTPRepository extends AbstractSshBasedRepository {
      // this must be a long to ensure the multiplication done below uses longs
      // instead of ints which are not big enough to hold the result
      private static final long MILLIS_PER_SECOND = 1000;
  
      private final class MyProgressMonitor implements SftpProgressMonitor {
          private long totalLength;
  
          public void init(int op, String src, String dest, long max) {
              totalLength = max;
              fireTransferStarted(max);
          }
  
          public void end() {
              fireTransferCompleted(totalLength);
          }
  
          public boolean count(long count) {
              fireTransferProgress(count);
              return true;
          }
      }
  
      public SFTPRepository() {
      }
  
      public SFTPRepository(final TimeoutConstraint timeoutConstraint) {
          super(timeoutConstraint);
      }
  
      public Resource getResource(String source) {
          return new SFTPResource(this, source);
      }
  
      /**
       * This method is similar to getResource, except that the returned resource is fully initialized
       * (resolved in the sftp repository), and that the given string is a full remote path
       *
       * @param path
       *            the full remote path in the repository of the resource
       * @return a fully initialized resource, able to answer to all its methods without needing any
       *         further connection
       */
      @SuppressWarnings("unchecked")
      public Resource resolveResource(String path) {
          try {
              List<LsEntry> r = getSftpChannel(path).ls(getPath(path));
              if (r != null) {
                  SftpATTRS attrs = r.get(0).getAttrs();
                  return new BasicResource(path, true, attrs.getSize(),
                              attrs.getMTime() * MILLIS_PER_SECOND, false);
              }
          } catch (Exception e) {
              Message.debug("Error while resolving resource " + path, e);
              // silent fail, return nonexistent resource
          }
  
          return new BasicResource(path, false, 0, 0, false);
      }
  
      public InputStream openStream(SFTPResource resource) throws IOException {
          ChannelSftp c = getSftpChannel(resource.getName());
          try {
              String path = getPath(resource.getName());
              return c.get(path);
          } catch (SftpException | URISyntaxException e) {
              throw new IOException("impossible to open stream for " + resource + " on "
                      + getHost() + (e.getMessage() != null ? ": " + e.getMessage() : ""), e);
          }
      }
  
      public void get(String source, File destination) throws IOException {
          fireTransferInitiated(getResource(source), TransferEvent.REQUEST_GET);
          ChannelSftp c = getSftpChannel(source);
          try {
              String path = getPath(source);
              c.get(path, destination.getAbsolutePath(), new MyProgressMonitor());
          } catch (SftpException | URISyntaxException e) {
              throw new IOException("impossible to get " + source + " on " + getHost()
                      + (e.getMessage() != null ? ": " + e.getMessage() : ""), e);
          }
      }
  
      public void put(File source, String destination, boolean overwrite) throws IOException {
          fireTransferInitiated(getResource(destination), TransferEvent.REQUEST_PUT);
          ChannelSftp c = getSftpChannel(destination);
          try {
              String path = getPath(destination);
              if (!overwrite && checkExistence(path, c)) {
                  throw new IOException("destination file exists and overwrite == false");
              }
              if (path.indexOf('/') != -1) {
                  mkdirs(path.substring(0, path.lastIndexOf('/')), c);
              }
              c.put(source.getAbsolutePath(), path, new MyProgressMonitor());
          } catch (SftpException | URISyntaxException e) {
              throw new IOException(e.getMessage(), e);
          }
      }
  
      private void mkdirs(String directory, ChannelSftp c) throws SftpException {
          try {
              SftpATTRS att = c.stat(directory);
              if (att != null && att.isDir()) {
                  return;
              }
          } catch (SftpException ex) {
              if (directory.indexOf('/') != -1) {
                  mkdirs(directory.substring(0, directory.lastIndexOf('/')), c);
              }
              c.mkdir(directory);
          }
      }
  
      private String getPath(String sftpURI) throws URISyntaxException {
          String result = null;
          URI uri = new URI(sftpURI);
          result = uri.getPath();
  
          if (result == null) {
              throw new URISyntaxException(sftpURI, "Missing path in URI.");
          }
  
          return result;
      }
  
      @SuppressWarnings("unchecked")
      public List<String> list(String parent) throws IOException {
          try {
              ChannelSftp c = getSftpChannel(parent);
              String path = getPath(parent);
              Collection<LsEntry> r = c.ls(path);
              if (r != null) {
                  if (!path.endsWith("/")) {
                      path = parent + "/";
                  }
                  List<String> result = new ArrayList<>();
                  for (LsEntry entry : r) {
                      if (".".equals(entry.getFilename()) || "..".equals(entry.getFilename())) {
                          continue;
                      }
                      result.add(path + entry.getFilename());
                  }
                  return result;
              }
          } catch (SftpException | URISyntaxException e) {
              throw new IOException("Failed to return a listing for '" + parent + "'", e);
          }
          return null;
      }
  
      /**
       * Checks the existence for a remote file
       *
       * @param file
       *            to check
       * @param channel
       *            to use
       * @return true if file exists, false otherwise
       */
      private boolean checkExistence(String file, ChannelSftp channel) {
          try {
              return channel.stat(file) != null;
          } catch (SftpException ex) {
              return false;
          }
      }
  
      /**
       * Establish the connection to the server if not yet connected, and listen to ivy events for
       * closing connection when resolve is finished. Not meant to be used in multi threaded
       * environment.
       *
       * @return the ChannelSftp with which a connection is established
       * @throws IOException
       *             if any connection problem occurs
       */
      private ChannelSftp getSftpChannel(String pathOrUri) throws IOException {
          Session session = getSession(pathOrUri);
          String host = session.getHost();
          ChannelSftp channel = SshCache.getInstance().getChannelSftp(session);
          if (channel == null) {
              try {
                  channel = (ChannelSftp) session.openChannel("sftp");
                  channel.connect();
                  Message.verbose(":: SFTP :: connected to " + host + "!");
                  SshCache.getInstance().attachChannelSftp(session, channel);
              } catch (JSchException e) {
                  throw new IOException(e.getMessage(), e);
              }
          }
          return channel;
      }
  
      protected String getRepositoryScheme() {
          // use the Resolver type name here?
          // would be nice if it would be static, so we could use SFTPResolver.getTypeName()
          return "sftp";
      }
  }
  