using System;
using System.Linq;
using System.Text;
using Renci.SshNet.Channels;
using System.IO;
using Renci.SshNet.Common;
using System.Text.RegularExpressions;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace Renci.SshNet
{
///
/// Provides SCP client functionality.
///
public partial class ScpClient : BaseClient
{
private static readonly Regex _fileInfoRe = new Regex(@"C(?\d{4}) (?\d+) (?.+)");
private static char[] _byteToChar;
///
/// Gets or sets the operation timeout.
///
///
/// The timeout to wait until an operation completes. The default value is negative
/// one (-1) milliseconds, which indicates an infinite time-out period.
///
public TimeSpan OperationTimeout { get; set; }
///
/// Gets or sets the size of the buffer.
///
///
/// The size of the buffer. The default buffer size is 16384 bytes.
///
public uint BufferSize { get; set; }
///
/// Occurs when downloading file.
///
public event EventHandler Downloading;
///
/// Occurs when uploading file.
///
public event EventHandler Uploading;
#region Constructors
///
/// Initializes a new instance of the class.
///
/// The connection info.
/// is null.
public ScpClient(ConnectionInfo connectionInfo)
: this(connectionInfo, false)
{
}
///
/// Initializes a new instance of the class.
///
/// Connection host.
/// Connection port.
/// Authentication username.
/// Authentication password.
/// is null.
/// is invalid, or is null or contains whitespace characters.
/// is not within and .
[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
public ScpClient(string host, int port, string username, string password)
: this(new PasswordConnectionInfo(host, port, username, password), true)
{
}
///
/// Initializes a new instance of the class.
///
/// Connection host.
/// Authentication username.
/// Authentication password.
/// is null.
/// is invalid, or is null or contains whitespace characters.
public ScpClient(string host, string username, string password)
: this(host, ConnectionInfo.DEFAULT_PORT, username, password)
{
}
///
/// Initializes a new instance of the class.
///
/// Connection host.
/// Connection port.
/// Authentication username.
/// Authentication private key file(s) .
/// is null.
/// is invalid, -or- is null or contains whitespace characters.
/// is not within and .
[SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
public ScpClient(string host, int port, string username, params PrivateKeyFile[] keyFiles)
: this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), true)
{
}
///
/// Initializes a new instance of the class.
///
/// Connection host.
/// Authentication username.
/// Authentication private key file(s) .
/// is null.
/// is invalid, -or- is null or contains whitespace characters.
public ScpClient(string host, string username, params PrivateKeyFile[] keyFiles)
: this(host, ConnectionInfo.DEFAULT_PORT, username, keyFiles)
{
}
///
/// Initializes a new instance of the class.
///
/// The connection info.
/// Specified whether this instance owns the connection info.
/// is null.
///
/// If is true, then the
/// connection info will be disposed when this instance is disposed.
///
private ScpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
: base(connectionInfo, ownsConnectionInfo)
{
this.OperationTimeout = new TimeSpan(0, 0, 0, 0, -1);
this.BufferSize = 1024 * 16;
if (_byteToChar == null)
{
_byteToChar = new char[128];
var ch = '\0';
for (int i = 0; i < 128; i++)
{
_byteToChar[i] = ch++;
}
}
}
#endregion
///
/// Uploads the specified stream to the remote host.
///
/// Stream to upload.
/// Remote host file name.
public void Upload(Stream source, string path)
{
using (var input = new PipeStream())
using (var channel = this.Session.CreateClientChannel())
{
channel.DataReceived += delegate(object sender, ChannelDataEventArgs e)
{
input.Write(e.Data, 0, e.Data.Length);
input.Flush();
};
channel.Open();
int pathEnd = path.LastIndexOfAny(new[] { '\\', '/' });
if (pathEnd != -1)
{
// split the path from the file
string pathOnly = path.Substring(0, pathEnd);
string fileOnly = path.Substring(pathEnd + 1);
// Send channel command request
channel.SendExecRequest(string.Format("scp -t \"{0}\"", pathOnly));
this.CheckReturnCode(input);
path = fileOnly;
}
this.InternalUpload(channel, input, source, path);
channel.Close();
}
}
///
/// Downloads the specified file from the remote host to the stream.
///
/// Remote host file name.
/// The stream where to download remote file.
/// is null or contains whitespace characters.
/// is null.
/// Method calls made by this method to , may under certain conditions result in exceptions thrown by the stream.
public void Download(string filename, Stream destination)
{
if (filename.IsNullOrWhiteSpace())
throw new ArgumentException("filename");
if (destination == null)
throw new ArgumentNullException("destination");
using (var input = new PipeStream())
using (var channel = this.Session.CreateClientChannel())
{
channel.DataReceived += delegate(object sender, ChannelDataEventArgs e)
{
input.Write(e.Data, 0, e.Data.Length);
input.Flush();
};
channel.Open();
// Send channel command request
channel.SendExecRequest(string.Format("scp -f \"{0}\"", filename));
this.SendConfirmation(channel); // Send reply
var message = ReadString(input);
var match = _fileInfoRe.Match(message);
if (match.Success)
{
// Read file
this.SendConfirmation(channel); // Send reply
var mode = match.Result("${mode}");
var length = long.Parse(match.Result("${length}"));
var fileName = match.Result("${filename}");
this.InternalDownload(channel, input, destination, fileName, length);
}
else
{
this.SendConfirmation(channel, 1, string.Format("\"{0}\" is not valid protocol message.", message));
}
channel.Close();
}
}
private void InternalSetTimestamp(ChannelSession channel, Stream input, DateTime lastWriteTime, DateTime lastAccessime)
{
var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc);
var modificationSeconds = (long)(lastWriteTime - zeroTime).TotalSeconds;
var accessSeconds = (long)(lastAccessime - zeroTime).TotalSeconds;
this.SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds));
this.CheckReturnCode(input);
}
private void InternalUpload(ChannelSession channel, Stream input, Stream source, string filename)
{
var length = source.Length;
this.SendData(channel, string.Format("C0644 {0} {1}\n", length, Path.GetFileName(filename)));
var buffer = new byte[this.BufferSize];
var read = source.Read(buffer, 0, buffer.Length);
long totalRead = 0;
while (read > 0)
{
this.SendData(channel, buffer, read);
totalRead += read;
this.RaiseUploadingEvent(filename, length, totalRead);
read = source.Read(buffer, 0, buffer.Length);
}
this.SendConfirmation(channel);
this.CheckReturnCode(input);
}
private void InternalDownload(ChannelSession channel, Stream input, Stream output, string filename, long length)
{
var buffer = new byte[Math.Min(length, this.BufferSize)];
var needToRead = length;
do
{
var read = input.Read(buffer, 0, (int)Math.Min(needToRead, this.BufferSize));
output.Write(buffer, 0, read);
this.RaiseDownloadingEvent(filename, length, length - needToRead);
needToRead -= read;
}
while (needToRead > 0);
output.Flush();
// Raise one more time when file downloaded
this.RaiseDownloadingEvent(filename, length, length - needToRead);
// Send confirmation byte after last data byte was read
this.SendConfirmation(channel);
this.CheckReturnCode(input);
}
private void RaiseDownloadingEvent(string filename, long size, long downloaded)
{
if (this.Downloading != null)
{
this.Downloading(this, new ScpDownloadEventArgs(filename, size, downloaded));
}
}
private void RaiseUploadingEvent(string filename, long size, long uploaded)
{
if (this.Uploading != null)
{
this.Uploading(this, new ScpUploadEventArgs(filename, size, uploaded));
}
}
private void SendConfirmation(ChannelSession channel)
{
this.SendData(channel, new byte[] { 0 });
}
private void SendConfirmation(ChannelSession channel, byte errorCode, string message)
{
this.SendData(channel, new[] { errorCode });
this.SendData(channel, string.Format("{0}\n", message));
}
///
/// Checks the return code.
///
/// The output stream.
private void CheckReturnCode(Stream input)
{
var b = ReadByte(input);
if (b > 0)
{
var errorText = ReadString(input);
throw new ScpException(errorText);
}
}
partial void SendData(ChannelSession channel, string command);
private void SendData(ChannelSession channel, byte[] buffer, int length)
{
if (length == buffer.Length)
{
channel.SendData(buffer);
}
else
{
channel.SendData(buffer.Take(length).ToArray());
}
}
private void SendData(ChannelSession channel, byte[] buffer)
{
channel.SendData(buffer);
}
private static int ReadByte(Stream stream)
{
var b = stream.ReadByte();
while (b < 0)
{
Thread.Sleep(100);
b = stream.ReadByte();
}
return b;
}
private static string ReadString(Stream stream)
{
var hasError = false;
var sb = new StringBuilder();
var b = ReadByte(stream);
if (b == 1 || b == 2)
{
hasError = true;
b = ReadByte(stream);
}
var ch = _byteToChar[b];
while (ch != '\n')
{
sb.Append(ch);
b = ReadByte(stream);
ch = _byteToChar[b];
}
if (hasError)
throw new ScpException(sb.ToString());
return sb.ToString();
}
}
}