All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.helger.jsch.scp.ScpConnection Maven / Gradle / Ivy

The newest version!
/*
 * Copyright (C) 2016-2024 Philip Helger (www.helger.com)
 * philip[at]helger[dot]com
 *
 * 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 com.helger.jsch.scp;

import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;

import javax.annotation.Nonnegative;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.helger.commons.ValueEnforcer;
import com.helger.commons.annotation.Nonempty;
import com.helger.commons.collection.NonBlockingStack;
import com.helger.commons.io.stream.StreamHelper;
import com.helger.jsch.session.ISessionFactory;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;

/**
 * Based on protocol information found
 * here
 *
 * @author LTHEISEN
 */
public class ScpConnection implements Closeable
{
  private static final Logger LOGGER = LoggerFactory.getLogger (ScpConnection.class);

  private final ChannelExec m_aChannel;
  private final NonBlockingStack  m_aEntryStack = new NonBlockingStack <> ();
  private final InputStream m_aIS;
  private final OutputStream m_aOS;
  private final Session m_aSession;

  public ScpConnection (@Nonnull final ISessionFactory aSessionFactory,
                        final String sPath,
                        @Nonnull final EScpMode eScpMode,
                        @Nullable final ECopyMode eCopyMode) throws JSchException, IOException
  {
    ValueEnforcer.notNull (aSessionFactory, "SessionFactory");
    ValueEnforcer.notNull (eScpMode, "ScpMode");

    m_aSession = aSessionFactory.createSession ();
    if (!m_aSession.isConnected ())
    {
      if (LOGGER.isDebugEnabled ())
        LOGGER.debug ("connecting session");
      m_aSession.connect ();
    }

    final String sCommand = _getCommand (eScpMode, eCopyMode, sPath);
    m_aChannel = (ChannelExec) m_aSession.openChannel ("exec");

    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("setting exec command to '" + sCommand + "'");
    m_aChannel.setCommand (sCommand);

    if (LOGGER.isDebugEnabled ())
      LOGGER.debug ("connecting channel");
    m_aChannel.connect ();

    m_aOS = m_aChannel.getOutputStream ();
    m_aIS = m_aChannel.getInputStream ();

    if (eScpMode == EScpMode.FROM)
      _writeAck ();
    else
      if (eScpMode == EScpMode.TO)
        _checkAck ();
  }

  @Nonnull
  @Nonempty
  private static String _getCommand (@Nonnull final EScpMode eScpMode,
                                     @Nullable final ECopyMode eCopyMode,
                                     @Nonnull final String sPath)
  {
    final StringBuilder aSB;
    switch (eScpMode)
    {
      case TO:
        aSB = new StringBuilder ("scp -tq");
        break;
      case FROM:
        aSB = new StringBuilder ("scp -fq");
        break;
      default:
        throw new IllegalStateException ();
    }

    if (eCopyMode == ECopyMode.RECURSIVE)
      aSB.append ('r');
    return aSB.append (' ').append (sPath).toString ();
  }

  /**
   * Throws an JSchIOException if ack was in error. Ack codes are:
   *
   * 
   *   0 for success,
   *   1 for error,
   *   2 for fatal error
   * 
* * Also throws, IOException if unable to read from the InputStream. If nothing * was thrown, ack was a success. */ private int _checkAck () throws IOException { if (LOGGER.isTraceEnabled ()) LOGGER.trace ("wait for ack"); final int b = m_aIS.read (); if (LOGGER.isDebugEnabled ()) LOGGER.debug ("ack response: '" + b + "'"); if (b == 1 || b == 2) { final StringBuilder aSB = new StringBuilder (); int c; while ((c = m_aIS.read ()) != '\n') { aSB.append ((char) c); } if (b == 1 || b == 2) throw new IOException (aSB.toString ()); } return b; } public void close () throws IOException { IOException aToThrow = null; try { while (!m_aEntryStack.isEmpty ()) { m_aEntryStack.pop ().complete (); } } catch (final IOException e) { aToThrow = e; } StreamHelper.close (m_aOS); StreamHelper.close (m_aIS); if (m_aChannel != null && m_aChannel.isConnected ()) { m_aChannel.disconnect (); } if (m_aSession != null && m_aSession.isConnected ()) { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("disconnecting session"); m_aSession.disconnect (); } if (aToThrow != null) throw aToThrow; } public void closeEntry () throws IOException { m_aEntryStack.pop ().complete (); } @Nullable public InputStream getCurrentInputStream () { if (m_aEntryStack.isEmpty ()) return null; final ICurrentEntry aEntry = m_aEntryStack.peek (); return aEntry instanceof InputStream ? (InputStream) aEntry : null; } @Nullable public OutputStream getCurrentOuputStream () { if (m_aEntryStack.isEmpty ()) return null; final ICurrentEntry aEntry = m_aEntryStack.peek (); return aEntry instanceof OutputStream ? (OutputStream) aEntry : null; } @Nullable public ScpEntry getNextEntry () throws IOException { if (!m_aEntryStack.isEmpty () && !m_aEntryStack.peek ().isDirectoryEntry ()) { closeEntry (); } final ScpEntry entry = _parseMessage (); if (entry == null) return null; if (entry.isEndOfDirectory ()) { while (!m_aEntryStack.isEmpty ()) { final boolean isDirectory = m_aEntryStack.peek ().isDirectoryEntry (); closeEntry (); if (isDirectory) break; } } else if (entry.isDirectory ()) { m_aEntryStack.push (new InputDirectoryEntry ()); } else { m_aEntryStack.push (new EntryInputStream (entry)); } return entry; } /** * Parses SCP protocol messages, for example: * *
   *     File:          C0640 13 test.txt
   *     Directory:     D0750 0 testdir
   *     End Directory: E
   * 
* * @return An ScpEntry for a file (C), directory (D), end of directory (E), or * null when no more messages are available. * @throws IOException */ @Nullable private ScpEntry _parseMessage () throws IOException { final int ack = _checkAck (); if (ack == -1) { // end of stream return null; } final char type = (char) ack; final ScpEntry scpEntry; if (type == 'E') { scpEntry = ScpEntry.newEndOfDirectory (); // read and discard the \n _readMessageSegment (); } else if (type == 'C' || type == 'D') { final String mode = _readMessageSegment (); final String sizeString = _readMessageSegment (); if (sizeString == null) return null; final long size = Long.parseLong (sizeString); final String name = _readMessageSegment (); if (name == null) return null; scpEntry = type == 'C' ? ScpEntry.newFile (name, size, mode) : ScpEntry.newDirectory (name, mode); } else { throw new UnsupportedOperationException ("unknown protocol message type " + type); } if (LOGGER.isDebugEnabled ()) LOGGER.debug ("read '" + scpEntry.getAsString () + "'"); return scpEntry; } public void putNextEntry (final String name) throws IOException { putNextEntry (ScpEntry.newDirectory (name)); } public void putNextEntry (final String name, final long size) throws IOException { putNextEntry (ScpEntry.newFile (name, size)); } public void putNextEntry (@Nonnull final ScpEntry aEntry) throws IOException { if (aEntry.isEndOfDirectory ()) { while (!m_aEntryStack.isEmpty ()) { final boolean bIsDirectory = m_aEntryStack.peek ().isDirectoryEntry (); closeEntry (); if (bIsDirectory) break; } return; } else if (!m_aEntryStack.isEmpty ()) { final ICurrentEntry currentEntry = m_aEntryStack.peek (); if (!currentEntry.isDirectoryEntry ()) { // auto close previous file entry closeEntry (); } } if (aEntry.isDirectory ()) { m_aEntryStack.push (new OutputDirectoryEntry (aEntry)); } else { m_aEntryStack.push (new EntryOutputStream (aEntry)); } } @Nonnull private String _readMessageSegment () throws IOException { final byte [] buffer = new byte [1024]; int bytesRead = 0; for (;; bytesRead++) { final byte b = (byte) m_aIS.read (); if (b == -1) return null; // end of stream if (b == ' ' || b == '\n') break; buffer[bytesRead] = b; } return new String (buffer, 0, bytesRead, StandardCharsets.US_ASCII); } private void _writeAck () throws IOException { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("writing ack"); m_aOS.write ((byte) 0); m_aOS.flush (); } private void _writeMessage (final String message) throws IOException { _writeMessage (message.getBytes (StandardCharsets.US_ASCII)); } private void _writeMessage (final byte... message) throws IOException { if (LOGGER.isDebugEnabled ()) LOGGER.debug ("writing message: '" + new String (message, StandardCharsets.US_ASCII) + "'"); m_aOS.write (message); m_aOS.flush (); _checkAck (); } private interface ICurrentEntry { void complete () throws IOException; boolean isDirectoryEntry (); } private class InputDirectoryEntry implements ICurrentEntry { private InputDirectoryEntry () throws IOException { _writeAck (); } public void complete () throws IOException { _writeAck (); } public boolean isDirectoryEntry () { return true; } } private class OutputDirectoryEntry implements ICurrentEntry { private OutputDirectoryEntry (final ScpEntry entry) throws IOException { _writeMessage ("D" + entry.getMode () + " 0 " + entry.getName () + "\n"); } public void complete () throws IOException { _writeMessage ("E\n"); } public boolean isDirectoryEntry () { return true; } } private class EntryInputStream extends InputStream implements ICurrentEntry { private final ScpEntry m_aEntry; private long m_nIOCount; private boolean m_bClosed; public EntryInputStream (final ScpEntry entry) throws IOException { m_aEntry = entry; m_nIOCount = 0L; _writeAck (); m_bClosed = false; } @Override public void close () throws IOException { if (!m_bClosed) { if (!_isComplete ()) { throw new IOException ("stream not finished (" + m_nIOCount + "!=" + m_aEntry.getSize () + ")"); } _writeAck (); _checkAck (); m_bClosed = true; } } public void complete () throws IOException { close (); } private void _increment (@Nonnegative final int nCount) { m_nIOCount += nCount; } private boolean _isComplete () { return m_nIOCount == m_aEntry.getSize (); } public boolean isDirectoryEntry () { return false; } @Override public int read () throws IOException { if (_isComplete ()) return -1; _increment (1); return m_aIS.read (); } @Override public int read (final byte [] aBuf, final int nOfs, final int nLen) throws IOException { if (_isComplete ()) return -1; final int nBytesRead = m_aIS.read (aBuf, nOfs, nLen); _increment (nBytesRead); return nBytesRead; } } private class EntryOutputStream extends OutputStream implements ICurrentEntry { private final ScpEntry m_aEntry; private long m_nIOCount; private boolean m_bClosed; public EntryOutputStream (final ScpEntry entry) throws IOException { m_aEntry = entry; m_nIOCount = 0L; _writeMessage ("C" + entry.getMode () + " " + entry.getSize () + " " + entry.getName () + "\n"); m_bClosed = false; } @Override public void close () throws IOException { if (!m_bClosed) { if (!_isComplete ()) throw new IOException ("stream not finished (" + m_nIOCount + "!=" + m_aEntry.getSize () + ")"); _writeMessage ((byte) 0); m_bClosed = true; } } public void complete () throws IOException { close (); } private void _increment (@Nonnegative final int n) throws IOException { if (_isComplete ()) throw new IOException ("too many bytes written for file " + m_aEntry.getName ()); m_nIOCount += n; } private boolean _isComplete () { return m_nIOCount == m_aEntry.getSize (); } public boolean isDirectoryEntry () { return false; } @Override public void write (final int b) throws IOException { _increment (1); m_aOS.write (b); } @Override public void write (final byte [] aBuf, final int nOfs, final int nLen) throws IOException { _increment (nLen); m_aOS.write (aBuf, nOfs, nLen); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy