diff --git a/Renci.SshClient/Renci.SshClient/Common/FtpFileInfo.cs b/Renci.SshClient/Renci.SshClient/Common/FtpFileInfo.cs deleted file mode 100644 index 7a479415..00000000 --- a/Renci.SshClient/Renci.SshClient/Common/FtpFileInfo.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace Renci.SshClient.Common -{ - public class FtpFileInfo - { - public string Name { get; set; } - - public string FullName { get; set; } - - public DateTime? CreationTime { get; set; } - - public DateTime? LastAccessTime { get; set; } - - public DateTime? LastModifyTime { get; set; } - - public ulong? Size { get; set; } - - public uint? UserId { get; set; } - - public uint? GroupId { get; set; } - - public uint? Permissions { get; set; } - - public IDictionary Extentions { get; set; } - - } -} diff --git a/Renci.SshClient/Renci.SshClient/Messages/Sftp/AttrsMessage.cs b/Renci.SshClient/Renci.SshClient/Messages/Sftp/AttrsMessage.cs index 097bf672..1b1b3a7f 100644 --- a/Renci.SshClient/Renci.SshClient/Messages/Sftp/AttrsMessage.cs +++ b/Renci.SshClient/Renci.SshClient/Messages/Sftp/AttrsMessage.cs @@ -1,7 +1,7 @@  namespace Renci.SshClient.Messages.Sftp { - internal class AttrsMessage : SftpRequestMessage + internal class AttributesMessage : SftpRequestMessage { public override SftpMessageTypes SftpMessageType { diff --git a/Renci.SshClient/Renci.SshClient/Messages/Sftp/NameMessage.cs b/Renci.SshClient/Renci.SshClient/Messages/Sftp/NameMessage.cs index e8f8eea3..c130f325 100644 --- a/Renci.SshClient/Renci.SshClient/Messages/Sftp/NameMessage.cs +++ b/Renci.SshClient/Renci.SshClient/Messages/Sftp/NameMessage.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using Renci.SshClient.Common; +using Renci.SshClient.Sftp; namespace Renci.SshClient.Messages.Sftp { @@ -13,35 +13,37 @@ namespace Renci.SshClient.Messages.Sftp public uint Count { get; set; } - public IList Files { get; set; } + public IEnumerable Files { get; set; } public NameMessage() { - this.Files = new List(); } protected override void LoadData() { base.LoadData(); this.Count = this.ReadUInt32(); + var files = new List(); for (int i = 0; i < this.Count; i++) { var fileName = this.ReadString(); var fullName = this.ReadString(); var attribute = this.ReadAttributes(); - this.Files.Add(new FtpFileInfo + // TODO: Complete files info population + files.Add(new SftpFile { Name = fileName, - FullName = fullName, - Size = attribute.Size, - UserId = attribute.UserId, - GroupId = attribute.GroupId, - LastAccessTime = attribute.AccessTime, - LastModifyTime = attribute.ModifyTime, + AbsolutePath = fullName, + //Size = attribute.Size, + //UserId = attribute.UserId, + //GroupId = attribute.GroupId, + //AccessedTime = attribute.AccessTime, + //ModifiedTime = attribute.ModifyTime, Extentions = attribute.Extentions, }); } + this.Files = files; } protected override void SaveData() diff --git a/Renci.SshClient/Renci.SshClient/Messages/Sftp/SftpMessage.cs b/Renci.SshClient/Renci.SshClient/Messages/Sftp/SftpMessage.cs index 7813da17..36ab2752 100644 --- a/Renci.SshClient/Renci.SshClient/Messages/Sftp/SftpMessage.cs +++ b/Renci.SshClient/Renci.SshClient/Messages/Sftp/SftpMessage.cs @@ -52,7 +52,7 @@ namespace Renci.SshClient.Messages.Sftp SftpMessage._sftpMessageTypes.Add(SftpMessageTypes.Handle, new LoadFunc(Load)); SftpMessage._sftpMessageTypes.Add(SftpMessageTypes.Data, new LoadFunc(Load)); SftpMessage._sftpMessageTypes.Add(SftpMessageTypes.Name, new LoadFunc(Load)); - SftpMessage._sftpMessageTypes.Add(SftpMessageTypes.Attrs, new LoadFunc(Load)); + SftpMessage._sftpMessageTypes.Add(SftpMessageTypes.Attrs, new LoadFunc(Load)); SftpMessage._sftpMessageTypes.Add(SftpMessageTypes.Extended, new LoadFunc(Load)); SftpMessage._sftpMessageTypes.Add(SftpMessageTypes.ExtendedReply, new LoadFunc(Load)); } diff --git a/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj b/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj index b76ae6ef..fd32425d 100644 --- a/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj +++ b/Renci.SshClient/Renci.SshClient/Renci.SshClient.csproj @@ -84,7 +84,19 @@ - + + + + + + + + + + + + + @@ -104,7 +116,6 @@ - @@ -229,9 +240,7 @@ - - diff --git a/Renci.SshClient/Renci.SshClient/Sftp.cs b/Renci.SshClient/Renci.SshClient/Sftp.cs deleted file mode 100644 index 898d7ca4..00000000 --- a/Renci.SshClient/Renci.SshClient/Sftp.cs +++ /dev/null @@ -1,273 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Text; -using System.Threading; -using Renci.SshClient.Channels; -using Renci.SshClient.Common; -using Renci.SshClient.Messages.Sftp; - -namespace Renci.SshClient -{ - public class Sftp - { - private readonly ChannelSession _channel; - - private readonly Session _session; - - private StringBuilder _packetData; - - private uint _requestId; - - private Exception _exception; - - private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(false); - - private EventWaitHandle _sessionErrorOccuredWaitHandle = new AutoResetEvent(false); - - private IDictionary> _requestActions; - - private string _remoteCurrentDir; - - public int OperationTimeout { get; set; } - - internal Sftp(Session session) - { - this.OperationTimeout = -1; - this._requestActions = new Dictionary>(); - this._session = session; - this._session.ErrorOccured += Session_ErrorOccured; - this._session.Disconnected += Session_Disconnected; - this._channel = session.CreateChannel(); - this._channel.DataReceived += Channel_DataReceived; - this._channel.Open(); - this._channel.SendSubsystemRequest("sftp"); - - this.SendMessage(new InitMessage - { - Version = 3, - }); - - this.WaitHandle(this._sftpVersionConfirmed); - - this.SendMessage(new RealPathMessage - { - Path = ".", - }, (m) => - { - var nameMessage = m as NameMessage; - if (nameMessage != null) - { - this._remoteCurrentDir = nameMessage.Files.First().Name; - } - else - { - throw new InvalidOperationException(""); - } - }); - } - - public IAsyncResult BeginListDirectory(string path, AsyncCallback callback, object state) - { - var asyncResult = new SftpAsyncResult(this._channel, callback, state); - - this.SendMessage(new OpenDirMessage(path), (m) => - { - var message = m as HandleMessage; - if (message == null) - throw new InvalidOperationException("Handle message is not expected."); - - this.SendMessage(new ReadDirMessage(message.Handle), (m1) => - { - var message1 = m1 as NameMessage; - if (message1 == null) - throw new InvalidOperationException("Handle message is not expected."); - asyncResult.Names = message1.Files; - asyncResult.IsCompleted = true; - }); - }); - - return asyncResult; - - - } - - public IEnumerable EndListDirectory(IAsyncResult result) - { - var r = result as SftpAsyncResult; - if (r == null) - { - throw new ArgumentException("Invalid IAsyncResult parameter."); - } - - r.AsyncWaitHandle.WaitOne(); - - return r.Names; - } - - - private void Channel_DataReceived(object sender, Common.ChannelDataEventArgs e) - { - if (this._packetData == null) - { - var packetLength = (uint)(e.Data[0] << 24 | e.Data[1] << 16 | e.Data[2] << 8 | e.Data[3]); - - this._packetData = new StringBuilder((int)packetLength, (int)packetLength); - this._packetData.Append(e.Data.GetSshBytes().Skip(4).GetSshString()); - } - else - { - this._packetData.Append(e.Data); - } - - if (this._packetData.Length < this._packetData.MaxCapacity) - { - // Wait for more packet data - return; - } - - dynamic sftpMessage = SftpMessage.Load(this._packetData.ToString().GetSshBytes()); - - this._packetData = null; - - this.HandleMessage(sftpMessage); - } - - private void HandleMessage(InitMessage message) - { - throw new InvalidOperationException("Init message should not be received by client."); - } - - private void HandleMessage(VersionMessage message) - { - if (message.Version == 3) - { - this._sftpVersionConfirmed.Set(); - } - else - { - throw new NotSupportedException(string.Format("Server SFTP version {0} is not supported.", message.Version)); - } - } - - private void HandleMessage(SftpRequestMessage message) - { - if (this._requestActions.ContainsKey(message.RequestId)) - { - var action = this._requestActions[message.RequestId]; - action(message); - this._requestActions.Remove(message.RequestId); - } - else - { - throw new InvalidOperationException(string.Format("Request #{0} is invalid.", message.RequestId)); - } - } - - private void Session_Disconnected(object sender, EventArgs e) - { - } - - private void Session_ErrorOccured(object sender, ErrorEventArgs e) - { - this._exception = e.GetException(); - - this._sessionErrorOccuredWaitHandle.Set(); - } - - private void SendMessage(SftpMessage sftpMessage) - { - var message = new SftpDataMessage - { - LocalChannelNumber = this._channel.RemoteChannelNumber, - Message = sftpMessage, - }; - - this._session.SendMessage(message); - } - - private void SendMessage(SftpRequestMessage sftpMessage, Action action) - { - sftpMessage.RequestId = this._requestId++; - - var message = new SftpDataMessage - { - LocalChannelNumber = this._channel.RemoteChannelNumber, - Message = sftpMessage, - }; - - this._session.SendMessage(message); - - this._requestActions.Add(sftpMessage.RequestId, action); - } - - private void WaitHandle(WaitHandle waitHandle) - { - var waitHandles = new WaitHandle[] - { - this._sessionErrorOccuredWaitHandle, - waitHandle, - }; - - var index = EventWaitHandle.WaitAny(waitHandles, this.OperationTimeout); - - if (index < 1) - { - throw this._exception; - } - else if (index > 1) - { - // throw time out error - throw new SshOperationTimeoutException(string.Format("Sftp operation has timed out.")); - } - } - - - //public void UploadFile(Stream source, string fileName) - //{ - // this.Channel.UploadFile(source, fileName); - //} - - //public void UploadFile(string source, string fileName) - //{ - // using (var sourceFile = File.OpenRead(source)) - // { - // this.Channel.UploadFile(sourceFile, fileName); - // } - //} - - //public void DownloadFile(string fileName, Stream destination) - //{ - // this.Channel.DownloadFile(fileName, destination); - //} - - //public void DownloadFile(string fileName, string destination) - //{ - // using (var destinationFile = File.Create(destination)) - // { - // this.Channel.DownloadFile(fileName, destinationFile); - // } - //} - - //public void RemoveFile(string fileName) - //{ - // this.Channel.RemoveFile(fileName); - //} - - //public void RenameFile(string oldFileName, string newFileName) - //{ - // this.Channel.RenameFile(oldFileName, newFileName); - //} - - //public void CreateDirectory(string directoryName) - //{ - // this.Channel.CreateDirectory(directoryName); - //} - - //public void RemoveDirectory(string directoryName) - //{ - // this.Channel.RemoveDirectory(directoryName); - //} - } -} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/CreateRemoteDirectoryCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/CreateRemoteDirectoryCommand.cs new file mode 100644 index 00000000..8116c000 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/CreateRemoteDirectoryCommand.cs @@ -0,0 +1,28 @@ +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class CreateRemoteDirectoryCommand : SftpCommand + { + private string _path; + + public CreateRemoteDirectoryCommand(SftpSession sftpSession, string path) + : base(sftpSession) + { + this._path = path; + } + + protected override void OnExecute() + { + this.SendMkDirMessage(this._path); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/DownloadFileCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/DownloadFileCommand.cs new file mode 100644 index 00000000..38743f9e --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/DownloadFileCommand.cs @@ -0,0 +1,61 @@ +using System.IO; +using System.Linq; +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class DownloadFileCommand : SftpCommand + { + private string _path; + private Stream _output; + private string _handle; + private ulong _offset = 0; + private uint _bufferSize = 1024; + private string _data; + + public DownloadFileCommand(SftpSession sftpSession, string path, Stream output) + : base(sftpSession) + { + this._path = path; + this._output = output; + // TODO: Make this field to be customizable + this._bufferSize = 1024; + } + + protected override void OnExecute() + { + this.SendOpenMessage(this._path, Flags.Read); + } + + protected override void OnHandle(string handle) + { + base.OnHandle(handle); + + this._handle = handle; + + this.SendReadMessage(this._handle, this._offset, this._bufferSize); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + base.OnStatus(statusCode, errorMessage, language); + + if (statusCode == StatusCodes.Eof) + { + this.CompleteExecution(); + } + } + + protected override void OnData(string data, bool isEof) + { + base.OnData(data, isEof); + + var fileData = data.GetSshBytes().ToArray(); + this._output.Write(fileData, 0, fileData.Length); + this._output.Flush(); + this._offset += (ulong)fileData.Length; + + this.SendReadMessage(this._handle, this._offset, this._bufferSize); + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/ListDirectoryCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/ListDirectoryCommand.cs new file mode 100644 index 00000000..8c9fca38 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/ListDirectoryCommand.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using Renci.SshClient.Common; +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class ListDirectoryCommand : SftpCommand + { + private string _path; + + private string _handle; + + public IEnumerable Files { get; private set; } + + public ListDirectoryCommand(SftpSession sftpSession, string path) + : base(sftpSession) + { + this._path = path; + } + + protected override void OnExecute() + { + this.SendOpenDirMessage(this._path); + } + + protected override void OnHandle(string handle) + { + base.OnHandle(handle); + + this._handle = handle; + + this.SendReadDirMessage(this._handle); + } + + protected override void OnName(IEnumerable files) + { + base.OnName(files); + + this.Files = files; + + this.CompleteExecution(); + + this.SendCloseMessage(this._handle); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + if (statusCode == StatusCodes.NoSuchFile) + { + throw new SshException(string.Format("Path '{0}' is not found.", this._path)); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/RemoveDirectoryCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/RemoveDirectoryCommand.cs new file mode 100644 index 00000000..b5195cc0 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/RemoveDirectoryCommand.cs @@ -0,0 +1,28 @@ +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class RemoveDirectoryCommand : SftpCommand + { + private string _path; + + public RemoveDirectoryCommand(SftpSession sftpSession, string path) + : base(sftpSession) + { + this._path = path; + } + + protected override void OnExecute() + { + this.SendRmDirMessage(this._path); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/RemoveFileCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/RemoveFileCommand.cs new file mode 100644 index 00000000..bead7821 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/RemoveFileCommand.cs @@ -0,0 +1,28 @@ +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class RemoveFileCommand : SftpCommand + { + private string _path; + + public RemoveFileCommand(SftpSession sftpSession, string path) + : base(sftpSession) + { + this._path = path; + } + + protected override void OnExecute() + { + this.SendRemoveMessage(this._path); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/RenameFileCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/RenameFileCommand.cs new file mode 100644 index 00000000..f575551a --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/RenameFileCommand.cs @@ -0,0 +1,30 @@ +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class RenameFileCommand : SftpCommand + { + private string _oldPath; + private string _newPath; + + public RenameFileCommand(SftpSession sftpSession, string oldPath, string newPath) + : base(sftpSession) + { + this._oldPath = oldPath; + this._newPath = newPath; + } + + protected override void OnExecute() + { + this.SendRenameMessage(this._oldPath, this._newPath); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/SftpAsyncResult.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpAsyncResult.cs similarity index 63% rename from Renci.SshClient/Renci.SshClient/SftpAsyncResult.cs rename to Renci.SshClient/Renci.SshClient/Sftp/SftpAsyncResult.cs index ac657d83..d8dee95e 100644 --- a/Renci.SshClient/Renci.SshClient/SftpAsyncResult.cs +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpAsyncResult.cs @@ -1,30 +1,32 @@ using System; -using System.Collections.Generic; using System.Threading; -using Renci.SshClient.Common; +using System.Threading.Tasks; -namespace Renci.SshClient +namespace Renci.SshClient.Sftp { public class SftpAsyncResult : IAsyncResult { private bool _isCompleted; - private Channels.ChannelSession _channelSession; - private AsyncCallback _callback; private EventWaitHandle _completedWaitHandle = new ManualResetEvent(false); - public IList Names { get; internal set; } + internal SftpCommand Command { get; private set; } - internal SftpAsyncResult(Channels.ChannelSession channelSession, AsyncCallback callback, object state) + internal SftpAsyncResult(SftpCommand command, AsyncCallback callback, object state) { - this._channelSession = channelSession; + this.Command = command; this._callback = callback; this.AsyncState = state; this.AsyncWaitHandle = _completedWaitHandle; } + internal T GetCommand() where T : SftpCommand + { + return this.Command as T; + } + #region IAsyncResult Members public object AsyncState { get; private set; } @@ -39,14 +41,20 @@ namespace Renci.SshClient { return this._isCompleted; } + internal set { this._isCompleted = value; + if (value) { if (this._callback != null) { - this._callback(this); + // Execute callback on new pool thread + Task.Factory.StartNew(() => + { + this._callback(this); + }); } this._completedWaitHandle.Set(); } diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SftpCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpCommand.cs new file mode 100644 index 00000000..57e60eb4 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpCommand.cs @@ -0,0 +1,298 @@ +using System; +using System.Collections.Generic; +using Renci.SshClient.Common; +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal abstract class SftpCommand + { + private SftpSession _sftpSession; + + private uint _requestId; + + private SftpAsyncResult _asyncResult; + + public SftpCommand(SftpSession sftpSession) + { + this._sftpSession = sftpSession; + this._sftpSession.AttributesMessageReceived += SftpSession_AttributesMessageReceived; + this._sftpSession.DataMessageReceived += SftpSession_DataMessageReceived; + this._sftpSession.HandleMessageReceived += SftpSession_HandleMessageReceived; + this._sftpSession.NameMessageReceived += SftpSession_NameMessageReceived; + this._sftpSession.StatusMessageReceived += SftpSession_StatusMessageReceived; + } + + public SftpAsyncResult BeginExecute(AsyncCallback callback, object state) + { + this._asyncResult = new SftpAsyncResult(this, callback, state); + + this.OnExecute(); + + return this._asyncResult; + } + + public void EndExecute(SftpAsyncResult result) + { + // TODO: Add timeout info here + this._sftpSession.WaitHandle(result.AsyncWaitHandle); + } + + public void Execute() + { + this.EndExecute(this.BeginExecute(null, null)); + } + + protected abstract void OnExecute(); + + protected virtual void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + } + + protected virtual void OnName(IEnumerable files) + { + } + + protected virtual void OnHandle(string handle) + { + } + + protected virtual void OnData(string data, bool isEof) + { + } + + protected virtual void OnAttributes(Attributes attributes) + { + } + + protected virtual void OnHandleClosed() + { + } + + protected void SendOpenMessage(string path, Flags flags) + { + this.SendMessage(new OpenMessage + { + Filename = path, + Flags = flags, + }); + } + + protected void SendCloseMessage(string handle) + { + this.SendMessage(new CloseMessage + { + Handle = handle, + }); + } + + protected void SendReadMessage(string handle, ulong offset, uint bufferSize) + { + this.SendMessage(new ReadMessage + { + Handle = handle, + Offset = offset, + Length = bufferSize, + }); + } + + protected void SendWriteMessage(string handle, ulong offset, string data) + { + this.SendMessage(new WriteMessage + { + Handle = handle, + Offset = offset, + Data = data, + }); + } + + protected void SendLStatMessage(string path) + { + this.SendMessage(new LStatMessage + { + Path = path, + }); + } + + protected void SendFStatMessage(string handle) + { + this.SendMessage(new FStatMessage + { + Handle = handle, + }); + } + + protected void SendSetStatMessage(string path, Attributes attributes) + { + this.SendMessage(new SetStatMessage + { + Path = path, + Attributes = attributes, + }); + } + + protected void SendFSetStatMessage(string handle, Attributes attributes) + { + this.SendMessage(new FSetStatMessage + { + Handle = handle, + Attributes = attributes, + }); + } + + protected void SendOpenDirMessage(string path) + { + this.SendMessage(new OpenDirMessage + { + Path = path, + }); + } + + protected void SendReadDirMessage(string handle) + { + this.SendMessage(new ReadDirMessage + { + Handle = handle, + }); + } + + protected void SendRemoveMessage(string filename) + { + this.SendMessage(new RemoveMessage + { + Filename = filename, + }); + } + + protected void SendMkDirMessage(string path) + { + this.SendMessage(new MkDirMessage + { + Path = path, + }); + } + + protected void SendRmDirMessage(string path) + { + this.SendMessage(new RmDirMessage + { + Path = path, + }); + } + + protected void SendRealPathMessage(string path) + { + this.SendMessage(new RealPathMessage + { + Path = path, + }); + } + + protected void SendStatMessage(string path) + { + this.SendMessage(new StatMessage + { + Path = path, + }); + } + + protected void SendRenameMessage(string oldPath, string newPath) + { + this.SendMessage(new RenameMessage + { + OldPath = oldPath, + NewPath = newPath, + }); + } + + protected void SendReadLinkMessage(string path) + { + this.SendMessage(new ReadLinkMessage + { + Path = path, + }); + } + + protected void SendSymLinkMessage(string existingPath, string newLinkPath, bool isSymLink) + { + this.SendMessage(new SymLinkMessage + { + ExistingPath = existingPath, + NewLinkPath = newLinkPath, + IsSymLink = isSymLink, + }); + } + + protected void CompleteExecution() + { + this._asyncResult.IsCompleted = true; + + this._sftpSession.AttributesMessageReceived -= SftpSession_AttributesMessageReceived; + this._sftpSession.DataMessageReceived -= SftpSession_DataMessageReceived; + this._sftpSession.HandleMessageReceived -= SftpSession_HandleMessageReceived; + this._sftpSession.NameMessageReceived -= SftpSession_NameMessageReceived; + this._sftpSession.StatusMessageReceived -= SftpSession_StatusMessageReceived; + } + + private void SftpSession_StatusMessageReceived(object sender, MessageEventArgs e) + { + if (this._requestId == e.Message.RequestId) + { + this.OnStatus(e.Message.StatusCode, e.Message.ErrorMessage, e.Message.Language); + } + + if (e.Message.StatusCode == StatusCodes.NoSuchFile || + e.Message.StatusCode == StatusCodes.PermissionDenied || + e.Message.StatusCode == StatusCodes.Failure || + e.Message.StatusCode == StatusCodes.BadMessage || + e.Message.StatusCode == StatusCodes.NoConnection || + e.Message.StatusCode == StatusCodes.ConnectionLost || + e.Message.StatusCode == StatusCodes.OperationUnsupported + ) + { + // Throw an exception if it was not handled by the command + throw new SshException(e.Message.ErrorMessage); + } + } + + private void SftpSession_NameMessageReceived(object sender, MessageEventArgs e) + { + if (this._requestId == e.Message.RequestId) + { + this.OnName(e.Message.Files); + } + } + + private void SftpSession_HandleMessageReceived(object sender, MessageEventArgs e) + { + if (this._requestId == e.Message.RequestId) + { + this.OnHandle(e.Message.Handle); + } + } + + private void SftpSession_DataMessageReceived(object sender, MessageEventArgs e) + { + if (this._requestId == e.Message.RequestId) + { + this.OnData(e.Message.Data, e.Message.IsEof); + } + } + + private void SftpSession_AttributesMessageReceived(object sender, MessageEventArgs e) + { + if (this._requestId == e.Message.RequestId) + { + this.OnAttributes(e.Message.Attributes); + } + } + + private void SendMessage(SftpRequestMessage message) + { + this._sftpSession.SendMessage(message); + + // Remeber command request id that was sent + this._requestId = message.RequestId; + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SftpFile.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpFile.cs new file mode 100644 index 00000000..ed05ef27 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpFile.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace Renci.SshClient.Sftp +{ + public class SftpFile + { + public SftpFile() + { + this.Attributes = new SftpFileAttributes(); + this.CreatedTime = DateTime.MinValue; + this.AccessedTime = DateTime.MinValue; + this.ModifiedTime = DateTime.MinValue; + this.Size = -1; + this.UserId = -1; + this.GroupId = -1; + this.Permissions = -1; + this.Extentions = new Dictionary(); + } + + public string Name { get; set; } + + public string AbsolutePath { get; set; } + + public SftpFileAttributes Attributes { get; set; } + + public string Filename { get; set; } + + public DateTime CreatedTime { get; set; } + + public DateTime AccessedTime { get; set; } + + public DateTime ModifiedTime { get; set; } + + public long Size { get; set; } + + public int UserId { get; set; } + + public int GroupId { get; set; } + + public int Permissions { get; set; } + + public IDictionary Extentions { get; set; } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SftpFileAttributes.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpFileAttributes.cs new file mode 100644 index 00000000..dd74a8a3 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpFileAttributes.cs @@ -0,0 +1,7 @@ + +namespace Renci.SshClient.Sftp +{ + public class SftpFileAttributes + { + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/SftpSession.cs b/Renci.SshClient/Renci.SshClient/Sftp/SftpSession.cs new file mode 100644 index 00000000..1e58e143 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/SftpSession.cs @@ -0,0 +1,245 @@ +using System; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading; +using Renci.SshClient.Channels; +using Renci.SshClient.Common; +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class SftpSession + { + private Session _session; + + private uint _requestId; + + private ChannelSession _channel; + + private StringBuilder _packetData; + + private Exception _exception; + + private EventWaitHandle _errorOccuredWaitHandle = new AutoResetEvent(false); + + private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(false); + + #region SFTP messages + + internal event EventHandler> StatusMessageReceived; + + internal event EventHandler> DataMessageReceived; + + internal event EventHandler> HandleMessageReceived; + + internal event EventHandler> NameMessageReceived; + + internal event EventHandler> AttributesMessageReceived; + + #endregion + + public int OperationTimeout { get; private set; } + + public SftpSession(Session session, int operationTimeout) + { + this._session = session; + this.OperationTimeout = operationTimeout; + } + + public void Connect() + { + this._channel = this._session.CreateChannel(); + + this._session.ErrorOccured += Session_ErrorOccured; + this._session.Disconnected += Session_Disconnected; + this._channel.DataReceived += Channel_DataReceived; + + this._channel.Open(); + + this._channel.SendSubsystemRequest("sftp"); + + this.SendMessage(new InitMessage + { + Version = 3, + }); + + this.WaitHandle(this._sftpVersionConfirmed); + + //this.SendMessage(new RealPathMessage + //{ + // Path = ".", + //}, (m) => + //{ + // var nameMessage = m as NameMessage; + // if (nameMessage != null) + // { + // this._remoteCurrentDir = nameMessage.Files.First().Name; + // } + // else + // { + // throw new InvalidOperationException(""); + // } + //}); + + + } + + public void Disconnect() + { + } + + internal void SendMessage(SftpRequestMessage sftpMessage) + { + sftpMessage.RequestId = this._requestId++; + + var message = new SftpDataMessage + { + LocalChannelNumber = this._channel.RemoteChannelNumber, + Message = sftpMessage, + }; + this._session.SendMessage(message); + } + + private void Channel_DataReceived(object sender, Common.ChannelDataEventArgs e) + { + if (this._packetData == null) + { + var packetLength = (uint)(e.Data[0] << 24 | e.Data[1] << 16 | e.Data[2] << 8 | e.Data[3]); + + this._packetData = new StringBuilder((int)packetLength, (int)packetLength); + this._packetData.Append(e.Data.GetSshBytes().Skip(4).GetSshString()); + } + else + { + this._packetData.Append(e.Data); + } + + if (this._packetData.Length < this._packetData.MaxCapacity) + { + // Wait for more packet data + return; + } + + dynamic sftpMessage = SftpMessage.Load(this._packetData.ToString().GetSshBytes()); + + this._packetData = null; + + try + { + // TODO: Check to run on different thread + this.HandleMessage(sftpMessage); + } + catch (Exception exp) + { + this._exception = exp; + this._errorOccuredWaitHandle.Set(); + } + } + + #region Handle SFTP incoming messages and raise appropriate events + + private void HandleMessage(InitMessage message) + { + throw new InvalidOperationException("Init message should not be received by client."); + } + + private void HandleMessage(VersionMessage message) + { + if (message.Version == 3) + { + this._sftpVersionConfirmed.Set(); + } + else + { + throw new NotSupportedException(string.Format("Server SFTP version {0} is not supported.", message.Version)); + } + } + + private void HandleMessage(StatusMessage message) + { + if (this.StatusMessageReceived != null) + { + this.StatusMessageReceived(this, new MessageEventArgs(message)); + } + } + + private void HandleMessage(DataMessage message) + { + if (this.DataMessageReceived != null) + { + this.DataMessageReceived(this, new MessageEventArgs(message)); + } + } + + private void HandleMessage(HandleMessage message) + { + if (this.HandleMessageReceived != null) + { + this.HandleMessageReceived(this, new MessageEventArgs(message)); + } + } + + private void HandleMessage(NameMessage message) + { + if (this.NameMessageReceived != null) + { + this.NameMessageReceived(this, new MessageEventArgs(message)); + } + } + + private void HandleMessage(AttributesMessage message) + { + if (this.AttributesMessageReceived != null) + { + this.AttributesMessageReceived(this, new MessageEventArgs(message)); + } + } + + #endregion + + private void Session_Disconnected(object sender, EventArgs e) + { + // TODO: Do something when session is closed and operation still in progress + } + + private void Session_ErrorOccured(object sender, ErrorEventArgs e) + { + this._exception = e.GetException(); + + this._errorOccuredWaitHandle.Set(); + } + + private void SendMessage(SftpMessage sftpMessage) + { + var message = new SftpDataMessage + { + LocalChannelNumber = this._channel.RemoteChannelNumber, + Message = sftpMessage, + }; + + this._session.SendMessage(message); + } + + internal void WaitHandle(WaitHandle waitHandle) + { + var waitHandles = new WaitHandle[] + { + this._errorOccuredWaitHandle, + waitHandle, + }; + + var index = EventWaitHandle.WaitAny(waitHandles, this.OperationTimeout); + + if (index < 1) + { + throw this._exception; + } + else if (index > 1) + { + // throw time out error + throw new SshOperationTimeoutException(string.Format("Sftp operation has timed out.")); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/Sftp/UploadFileCommand.cs b/Renci.SshClient/Renci.SshClient/Sftp/UploadFileCommand.cs new file mode 100644 index 00000000..13e65b29 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/Sftp/UploadFileCommand.cs @@ -0,0 +1,77 @@ +using System.IO; +using System.Linq; +using Renci.SshClient.Messages.Sftp; + +namespace Renci.SshClient.Sftp +{ + internal class UploadFileCommand : SftpCommand + { + private string _path; + private Stream _input; + private string _handle; + private byte[] _buffer; + private ulong _offset; + private bool _hasMoreData; + + public UploadFileCommand(SftpSession sftpSession, string path, Stream input) + : base(sftpSession) + { + this._path = path; + this._input = input; + // TODO: Make this field to be customizable + this._buffer = new byte[1024]; + } + + protected override void OnExecute() + { + this.SendOpenMessage(this._path, Flags.Write | Flags.CreateNewOrOpen | Flags.Truncate); + } + + protected override void OnHandle(string handle) + { + base.OnHandle(handle); + + this._handle = handle; + + this.SendData(); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + base.OnStatus(statusCode, errorMessage, language); + + if (statusCode == StatusCodes.Ok) + { + if (this._hasMoreData) + { + this.SendData(); + } + } + } + + protected override void OnHandleClosed() + { + base.OnHandleClosed(); + + this.CompleteExecution(); + } + + private void SendData() + { + var bytesRead = this._input.Read(this._buffer, 0, this._buffer.Length); + + this._hasMoreData = (bytesRead > 0); + + if (this._hasMoreData) + { + this.SendWriteMessage(this._handle, this._offset, this._buffer.Take(bytesRead).GetSshString()); + + this._offset += (ulong)bytesRead; + } + else + { + this.SendCloseMessage(this._handle); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshClient/SftpClient.cs b/Renci.SshClient/Renci.SshClient/SftpClient.cs new file mode 100644 index 00000000..15dd8ee5 --- /dev/null +++ b/Renci.SshClient/Renci.SshClient/SftpClient.cs @@ -0,0 +1,235 @@ +using System; +using System.Collections.Generic; +using System.IO; +using Renci.SshClient.Sftp; + +namespace Renci.SshClient +{ + public class SftpClient + { + private SftpSession _sftpSession; + + private Session _session; + + public ConnectionInfo ConnectionInfo { get; private set; } + + public bool IsConnected + { + get + { + if (this._session == null) + { + return false; + } + else + { + return this._session.IsConnected; + } + } + } + + public int OperationTimeout { get; set; } + + public int BufferSize { get; set; } + + #region Constructors + + public SftpClient(ConnectionInfo connectionInfo) + { + this.OperationTimeout = -1; + + this.ConnectionInfo = connectionInfo; + } + + public SftpClient(string host, int port, string username, string password) + : this(new ConnectionInfo + { + Host = host, + Port = port, + Username = username, + Password = password, + }) + { + } + + public SftpClient(string host, string username, string password) + : this(new ConnectionInfo + { + Host = host, + Username = username, + Password = password, + }) + { + } + + public SftpClient(string host, int port, string username, PrivateKeyFile keyFile) + : this(new ConnectionInfo + { + Host = host, + Port = port, + Username = username, + KeyFile = keyFile, + }) + { + } + + public SftpClient(string host, string username, PrivateKeyFile keyFile) + : this(new ConnectionInfo + { + Host = host, + Username = username, + KeyFile = keyFile, + }) + { + } + + #endregion + + public void Connect() + { + if (this._session == null) + { + this._session = new Session(this.ConnectionInfo); + } + this._session.Connect(); + + this._sftpSession = new SftpSession(this._session, this.OperationTimeout); + + this._sftpSession.Connect(); + } + + public void Disconnect() + { + this._sftpSession.Disconnect(); + + this._session.Disconnect(); + + this._session = null; + } + + #region List Directory + + public IAsyncResult BeginListDirectory(string path, AsyncCallback asyncCallback, object state) + { + var cmd = new ListDirectoryCommand(this._sftpSession, path); + return cmd.BeginExecute(asyncCallback, state); + } + + public IEnumerable EndListDirectory(IAsyncResult asyncResult) + { + var sftpAsyncResult = asyncResult as SftpAsyncResult; + + if (sftpAsyncResult == null) + { + throw new InvalidOperationException("Not valid IAsyncResult object."); + } + + sftpAsyncResult.Command.EndExecute(sftpAsyncResult); + + var cmd = sftpAsyncResult.Command as ListDirectoryCommand; + + if (cmd == null) + { + throw new InvalidOperationException("Not valid IAsyncResult object."); + } + return cmd.Files; + } + + public IEnumerable ListDirectory(string path) + { + return this.EndListDirectory(this.BeginListDirectory(path, null, null)); + } + + #endregion + + public void RenameFile(string oldFilename, string newFileName) + { + } + + public void RemoveFile(string filename) + { + } + + public void RemoveDirectory(string path) + { + } + + public void CreateDirectory(string path) + { + } + + #region Upload File + + public IAsyncResult BeginUploadFile(string filename, Stream input, AsyncCallback asyncCallback, object state) + { + var cmd = new UploadFileCommand(this._sftpSession, filename, input); + + return cmd.BeginExecute(asyncCallback, state); + } + + public void EndUploadFile(IAsyncResult asyncResult) + { + var sftpAsyncResult = asyncResult as SftpAsyncResult; + + if (sftpAsyncResult == null) + { + throw new InvalidOperationException("Not valid IAsyncResult object."); + } + + sftpAsyncResult.Command.EndExecute(sftpAsyncResult); + + var cmd = sftpAsyncResult.Command as UploadFileCommand; + + if (cmd == null) + { + throw new InvalidOperationException("Not valid IAsyncResult object."); + } + } + + public void UploadFile(string filename, Stream input) + { + this.EndUploadFile(this.BeginUploadFile(filename, input, null, null)); + } + + #endregion + + #region Download File + + public IAsyncResult BeginDownload(string filename, Stream output, AsyncCallback asyncCallback, object state) + { + var cmd = new DownloadFileCommand(this._sftpSession, filename, output); + + return cmd.BeginExecute(asyncCallback, state); + } + + public void EndDownload(IAsyncResult asyncResult) + { + var sftpAsyncResult = asyncResult as SftpAsyncResult; + + if (sftpAsyncResult == null) + { + throw new InvalidOperationException("Not valid IAsyncResult object."); + } + + sftpAsyncResult.Command.EndExecute(sftpAsyncResult); + + var cmd = sftpAsyncResult.Command as DownloadFileCommand; + + if (cmd == null) + { + throw new InvalidOperationException("Not valid IAsyncResult object."); + } + } + + public void Download(string filename, Stream output) + { + this.EndDownload(this.BeginDownload(filename, output, null, null)); + } + + #endregion + + public void ChangeDirectory(string path) + { + } + } +} \ No newline at end of file diff --git a/Renci.SshClient/Renci.SshClient/SshBase.cs b/Renci.SshClient/Renci.SshClient/SshBase.cs deleted file mode 100644 index 85c9b240..00000000 --- a/Renci.SshClient/Renci.SshClient/SshBase.cs +++ /dev/null @@ -1,69 +0,0 @@ - -namespace Renci.SshClient -{ - public abstract class SshBase - { - public ConnectionInfo ConnectionInfo { get; private set; } - - internal Session Session { get; private set; } - - public SshBase(ConnectionInfo connectionInfo) - { - this.ConnectionInfo = connectionInfo; - this.Session = new Session(this.ConnectionInfo); - } - - public SshBase(string host, int port, string username, string password) - : this(new ConnectionInfo - { - Host = host, - Port = port, - Username = username, - Password = password, - }) - { - } - - public SshBase(string host, string username, string password) - : this(new ConnectionInfo - { - Host = host, - Username = username, - Password = password, - }) - { - } - - public SshBase(string host, int port, string username, PrivateKeyFile keyFile) - : this(new ConnectionInfo - { - Host = host, - Port = port, - Username = username, - KeyFile = keyFile, - }) - { - } - - public SshBase(string host, string username, PrivateKeyFile keyFile) - : this(new ConnectionInfo - { - Host = host, - Username = username, - KeyFile = keyFile, - }) - { - } - - public void Connect() - { - this.Session.Connect(); - } - - public virtual void Disconnect() - { - this.Session.Disconnect(); - } - - } -}