diff --git a/Renci.SshClient/Renci.SshNet/Common/PortForwardEventArgs.cs b/Renci.SshClient/Renci.SshNet/Common/PortForwardEventArgs.cs index 09c3ce6c..763d5417 100644 --- a/Renci.SshClient/Renci.SshNet/Common/PortForwardEventArgs.cs +++ b/Renci.SshClient/Renci.SshNet/Common/PortForwardEventArgs.cs @@ -22,7 +22,7 @@ namespace Renci.SshNet.Common /// /// The host. /// The port. - public PortForwardEventArgs(string host, uint port) + internal PortForwardEventArgs(string host, uint port) { if (host == null) throw new ArgumentNullException("host"); diff --git a/Renci.SshClient/Renci.SshNet/Common/SshAuthenticationException.cs b/Renci.SshClient/Renci.SshNet/Common/SshAuthenticationException.cs index 77db4084..8a290e1f 100644 --- a/Renci.SshClient/Renci.SshNet/Common/SshAuthenticationException.cs +++ b/Renci.SshClient/Renci.SshNet/Common/SshAuthenticationException.cs @@ -12,6 +12,9 @@ namespace Renci.SshNet.Common [Serializable] public class SshAuthenticationException : SshException { + /// + /// Initializes a new instance of the class. + /// public SshAuthenticationException() { diff --git a/Renci.SshClient/Renci.SshNet/Renci.SshNet.csproj b/Renci.SshClient/Renci.SshNet/Renci.SshNet.csproj index 42cfa8df..27fc6771 100644 --- a/Renci.SshClient/Renci.SshNet/Renci.SshNet.csproj +++ b/Renci.SshClient/Renci.SshNet/Renci.SshNet.csproj @@ -128,7 +128,13 @@ + + + + + + @@ -162,6 +168,7 @@ + diff --git a/Renci.SshClient/Renci.SshNet/Sftp/CloseCommand.cs b/Renci.SshClient/Renci.SshNet/Sftp/CloseCommand.cs new file mode 100644 index 00000000..cec18251 --- /dev/null +++ b/Renci.SshClient/Renci.SshNet/Sftp/CloseCommand.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshNet.Sftp.Messages; + +namespace Renci.SshNet.Sftp +{ + internal class CloseCommand : SftpCommand + { + private byte[] _handle; + + public CloseCommand(SftpSession sftpSession, byte[] handle) + : base(sftpSession) + { + this._handle = handle; + } + + protected override void OnExecute() + { + this.SendCloseMessage(this._handle); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + base.OnStatus(statusCode, errorMessage, language); + + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshNet/Sftp/Messages/Flags.cs b/Renci.SshClient/Renci.SshNet/Sftp/Messages/Flags.cs index c76f481a..036a9ba2 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/Messages/Flags.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/Messages/Flags.cs @@ -2,6 +2,7 @@ { internal enum Flags { + None = 0x00000000, /// /// SSH_FXF_READ /// diff --git a/Renci.SshClient/Renci.SshNet/Sftp/Messages/SftpMessage.cs b/Renci.SshClient/Renci.SshNet/Sftp/Messages/SftpMessage.cs index e629622e..f58c4a3a 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/Messages/SftpMessage.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/Messages/SftpMessage.cs @@ -56,9 +56,11 @@ namespace Renci.SshNet.Sftp.Messages var flag = this.ReadUInt32(); + if ((flag & 0x00000001) == 0x00000001) // SSH_FILEXFER_ATTR_SIZE { attributes.Size = this.ReadUInt64(); + } if ((flag & 0x00000002) == 0x00000002) // SSH_FILEXFER_ATTR_UIDGID diff --git a/Renci.SshClient/Renci.SshNet/Sftp/OpenCommand.cs b/Renci.SshClient/Renci.SshNet/Sftp/OpenCommand.cs new file mode 100644 index 00000000..8da46b6b --- /dev/null +++ b/Renci.SshClient/Renci.SshNet/Sftp/OpenCommand.cs @@ -0,0 +1,50 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshNet.Sftp.Messages; +using Renci.SshNet.Common; +using System.Globalization; + +namespace Renci.SshNet.Sftp +{ + internal class OpenCommand : SftpCommand + { + private string _path; + + private Flags _flags; + + public byte[] Handle { get; private set; } + + public OpenCommand(SftpSession sftpSession, string path, Flags flags) + : base(sftpSession) + { + this._path = path; + this._flags = flags; + } + + protected override void OnExecute() + { + this.SendOpenMessage(this._path, this._flags); + } + + protected override void OnHandle(byte[] handle) + { + base.OnHandle(handle); + + this.Handle = handle; + + this.CompleteExecution(); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + base.OnStatus(statusCode, errorMessage, language); + + if (statusCode == StatusCodes.NoSuchFile) + { + throw new SshFileNotFoundException(string.Format(CultureInfo.CurrentCulture, "Path '{0}' is not found.", this._path)); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshNet/Sftp/ReadCommand.cs b/Renci.SshClient/Renci.SshNet/Sftp/ReadCommand.cs new file mode 100644 index 00000000..c912326d --- /dev/null +++ b/Renci.SshClient/Renci.SshNet/Sftp/ReadCommand.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshNet.Sftp.Messages; +using System.IO; + +namespace Renci.SshNet.Sftp +{ + internal class ReadCommand : SftpCommand + { + private byte[] _handle; + private ulong _offset; + private MemoryStream _output; + + public byte[] Data { get; private set; } + + public ReadCommand(SftpSession sftpSession, byte[] handle, ulong offset, uint bufferSize) + : base(sftpSession) + { + this._handle = handle; + this._offset = offset; + this._output = new MemoryStream((int)bufferSize); + } + + protected override void OnExecute() + { + this.SendReadMessage(this._handle, this._offset, (uint)this._output.Capacity); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + base.OnStatus(statusCode, errorMessage, language); + + if (statusCode == StatusCodes.Eof) + { + this.Data = this._output.ToArray(); + this.CompleteExecution(); + } + } + + protected override void OnData(byte[] data, bool isEof) + { + base.OnData(data, isEof); + + this._output.Write(data, 0, data.Length); + this._output.Flush(); + this._offset += (ulong)data.Length; + this.AsyncResult.DownloadedBytes = this._offset; + + uint bytesLeft = (uint)(this._output.Capacity - this._output.Position); + + this.SendReadMessage(this._handle, this._offset, bytesLeft); + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + if (disposing) + { + if (this._output != null) + { + this._output.Dispose(); + this._output = null; + } + } + } + } +} diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SetStatusCommand.cs b/Renci.SshClient/Renci.SshNet/Sftp/SetStatusCommand.cs new file mode 100644 index 00000000..2ff1530b --- /dev/null +++ b/Renci.SshClient/Renci.SshNet/Sftp/SetStatusCommand.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshNet.Sftp.Messages; + +namespace Renci.SshNet.Sftp +{ + internal class SetStatusCommand : SftpCommand + { + private string _path; + private byte[] _handle; + private SftpFileAttributes _attributes; + + public SetStatusCommand(SftpSession sftpSession, string path, SftpFileAttributes attributes) + : base(sftpSession) + { + this._path = path; + this._attributes = attributes; + } + + public SetStatusCommand(SftpSession sftpSession, byte[] handle, SftpFileAttributes attributes) + : base(sftpSession) + { + this._handle = handle; + this._attributes = attributes; + } + + protected override void OnExecute() + { + if (this._handle != null) + { + this.SendSetStatMessage(this._handle, this._attributes); + } + else if (this._path != null) + { + this.SendSetStatMessage(this._path, this._attributes); + } + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + base.OnStatus(statusCode, errorMessage, language); + + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + } +} diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpAsyncResult.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpAsyncResult.cs index 47ca8b10..b8273dcd 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/SftpAsyncResult.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpAsyncResult.cs @@ -81,6 +81,9 @@ namespace Renci.SshNet.Sftp Task.Factory.StartNew(() => { this._asyncCallback(this); }); } + /// + /// Ends asynchronous operation invocation. + /// public void EndInvoke() { // This method assumes that only 1 thread calls EndInvoke diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpCommand.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpCommand.cs index 4d016a88..a20c953b 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/SftpCommand.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpCommand.cs @@ -125,7 +125,7 @@ namespace Renci.SshNet.Sftp this.SendMessage(new SetStatMessage(this.SftpSession.NextRequestId, path, attributes)); } - protected void SendFSetStatMessage(byte[] handle, SftpFileAttributes attributes) + protected void SendSetStatMessage(byte[] handle, SftpFileAttributes attributes) { this.SendMessage(new FSetStatMessage(this.SftpSession.NextRequestId, handle, attributes)); } @@ -165,6 +165,11 @@ namespace Renci.SshNet.Sftp this.SendMessage(new StatMessage(this.SftpSession.NextRequestId, path)); } + protected void SendStatMessage(byte[] handle) + { + this.SendMessage(new FStatMessage(this.SftpSession.NextRequestId, handle)); + } + protected void SendRenameMessage(string oldPath, string newPath) { this.SendMessage(new RenameMessage(this.SftpSession.NextRequestId, oldPath, newPath)); diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpFile.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpFile.cs index cdbce0c1..ad26f841 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/SftpFile.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpFile.cs @@ -71,6 +71,9 @@ namespace Renci.SshNet.Sftp /// Attributes of the directory or file. internal SftpFile(SftpSession sftpSession, string fullName, SftpFileAttributes attributes) { + if (attributes == null) + throw new ArgumentNullException("attributes"); + this._sftpSession = sftpSession; this.Name = fullName.Substring(fullName.LastIndexOf('/') + 1); diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpFileAttributes.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpFileAttributes.cs new file mode 100644 index 00000000..f195c7e7 --- /dev/null +++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpFileAttributes.cs @@ -0,0 +1,358 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Renci.SshNet.Sftp +{ + public class SftpFileAttributes1 + { + #region Bitmask constats + + private static UInt32 S_IFMT = 0xF000; // bitmask for the file type bitfields + + private static UInt32 S_IFSOCK = 0xC000; // socket + + private static UInt32 S_IFLNK = 0xA000; // symbolic link + + private static UInt32 S_IFREG = 0x8000; // regular file + + private static UInt32 S_IFBLK = 0x6000; // block device + + private static UInt32 S_IFDIR = 0x4000; // directory + + private static UInt32 S_IFCHR = 0x2000; // character device + + private static UInt32 S_IFIFO = 0x1000; // FIFO + + private static UInt32 S_ISUID = 0x0800; // set UID bit + + private static UInt32 S_ISGID = 0x0400; // set-group-ID bit (see below) + + private static UInt32 S_ISVTX = 0x0200; // sticky bit (see below) + + private static UInt32 S_IRUSR = 0x0100; // owner has read permission + + private static UInt32 S_IWUSR = 0x0080; // owner has write permission + + private static UInt32 S_IXUSR = 0x0040; // owner has execute permission + + private static UInt32 S_IRGRP = 0x0020; // group has read permission + + private static UInt32 S_IWGRP = 0x0010; // group has write permission + + private static UInt32 S_IXGRP = 0x0008; // group has execute permission + + private static UInt32 S_IROTH = 0x0004; // others have read permission + + private static UInt32 S_IWOTH = 0x0002; // others have write permission + + private static UInt32 S_IXOTH = 0x0001; // others have execute permission + + #endregion + + private bool _isBitFiledsBitSet; + private bool _isUIDBitSet; + private bool _isGroupIDBitSet; + private bool _isStickyBitSet; + + /// + /// Gets or sets the time the current file or directory was last accessed. + /// + /// + /// The time that the current file or directory was last accessed. + /// + public DateTime LastAccessTime { get; private set; } + + /// + /// Gets or sets the time when the current file or directory was last written to. + /// + /// + /// The time the current file was last written. + /// + public DateTime LastWriteTime { get; private set; } + + /// + /// Gets or sets the size, in bytes, of the current file. + /// + /// + /// The size of the current file in bytes. + /// + public long Size { get; private set; } + + /// + /// Gets or sets file user id. + /// + /// + /// File user id. + /// + public int UserId { get; set; } + + /// + /// Gets or sets file group id. + /// + /// + /// File group id. + /// + public int GroupId { get; set; } + + /// + /// Gets a value indicating whether file represents a socket. + /// + /// + /// true if file represents a socket; otherwise, false. + /// + public bool IsSocket { get; private set; } + + /// + /// Gets a value indicating whether file represents a symbolic link. + /// + /// + /// true if file represents a symbolic link; otherwise, false. + /// + public bool IsSymbolicLink { get; private set; } + + /// + /// Gets a value indicating whether file represents a regular file. + /// + /// + /// true if file represents a regular file; otherwise, false. + /// + public bool IsRegularFile { get; private set; } + + /// + /// Gets a value indicating whether file represents a block device. + /// + /// + /// true if file represents a block device; otherwise, false. + /// + public bool IsBlockDevice { get; private set; } + + /// + /// Gets a value indicating whether file represents a directory. + /// + /// + /// true if file represents a directory; otherwise, false. + /// + public bool IsDirectory { get; private set; } + + /// + /// Gets a value indicating whether file represents a character device. + /// + /// + /// true if file represents a character device; otherwise, false. + /// + public bool IsCharacterDevice { get; private set; } + + /// + /// Gets a value indicating whether file represents a named pipe. + /// + /// + /// true if file represents a named pipe; otherwise, false. + /// + public bool IsNamedPipe { get; private set; } + + /// + /// Gets a value indicating whether the owner can read from this file. + /// + /// + /// true if owner can read from this file; otherwise, false. + /// + public bool OwnerCanRead { get; set; } + + /// + /// Gets a value indicating whether the owner can write into this file. + /// + /// + /// true if owner can write into this file; otherwise, false. + /// + public bool OwnerCanWrite { get; set; } + + /// + /// Gets a value indicating whether the owner can execute this file. + /// + /// + /// true if owner can execute this file; otherwise, false. + /// + public bool OwnerCanExecute { get; set; } + + /// + /// Gets a value indicating whether the group members can read from this file. + /// + /// + /// true if group members can read from this file; otherwise, false. + /// + public bool GroupCanRead { get; set; } + + /// + /// Gets a value indicating whether the group members can write into this file. + /// + /// + /// true if group members can write into this file; otherwise, false. + /// + public bool GroupCanWrite { get; set; } + + /// + /// Gets a value indicating whether the group members can execute this file. + /// + /// + /// true if group members can execute this file; otherwise, false. + /// + public bool GroupCanExecute { get; set; } + + /// + /// Gets a value indicating whether the others can read from this file. + /// + /// + /// true if others can read from this file; otherwise, false. + /// + public bool OthersCanRead { get; set; } + + /// + /// Gets a value indicating whether the others can write into this file. + /// + /// + /// true if others can write into this file; otherwise, false. + /// + public bool OthersCanWrite { get; set; } + + /// + /// Gets a value indicating whether the others can execute this file. + /// + /// + /// true if others can execute this file; otherwise, false. + /// + public bool OthersCanExecute { get; set; } + + /// + /// Gets or sets the extensions. + /// + /// + /// The extensions. + /// + public IDictionary Extensions { get; private set; } + + internal uint Permissions + { + get + { + uint permission = 0; + + if (this._isBitFiledsBitSet) + permission = permission | S_IFMT; + + if (this.IsSocket) + permission = permission | S_IFSOCK; + + if (this.IsSymbolicLink) + permission = permission | S_IFLNK; + + if (this.IsRegularFile) + permission = permission | S_IFREG; + + if (this.IsBlockDevice) + permission = permission | S_IFBLK; + + if (this.IsDirectory) + permission = permission | S_IFDIR; + + if (this.IsCharacterDevice) + permission = permission | S_IFCHR; + + if (this.IsNamedPipe) + permission = permission | S_IFIFO; + + if (this._isUIDBitSet) + permission = permission | S_ISUID; + + if (this._isGroupIDBitSet) + permission = permission | S_ISGID; + + if (this._isStickyBitSet) + permission = permission | S_ISVTX; + + if (this.OwnerCanRead) + permission = permission | S_IRUSR; + + if (this.OwnerCanWrite) + permission = permission | S_IWUSR; + + if (this.OwnerCanExecute) + permission = permission | S_IXUSR; + + if (this.GroupCanRead) + permission = permission | S_IRGRP; + + if (this.GroupCanWrite) + permission = permission | S_IWGRP; + + if (this.GroupCanExecute) + permission = permission | S_IXGRP; + + if (this.OthersCanRead) + permission = permission | S_IROTH; + + if (this.OthersCanWrite) + permission = permission | S_IWOTH; + + if (this.OthersCanExecute) + permission = permission | S_IXOTH; + + return permission; + } + + private set + { + this._isBitFiledsBitSet = ((value & S_IFMT) == S_IFMT); + + this.IsSocket = ((value & S_IFSOCK) == S_IFSOCK); + + this.IsSymbolicLink = ((value & S_IFLNK) == S_IFLNK); + + this.IsRegularFile = ((value & S_IFREG) == S_IFREG); + + this.IsBlockDevice = ((value & S_IFBLK) == S_IFBLK); + + this.IsDirectory = ((value & S_IFDIR) == S_IFDIR); + + this.IsCharacterDevice = ((value & S_IFCHR) == S_IFCHR); + + this.IsNamedPipe = ((value & S_IFIFO) == S_IFIFO); + + this._isUIDBitSet = ((value & S_ISUID) == S_ISUID); + + this._isGroupIDBitSet = ((value & S_ISGID) == S_ISGID); + + this._isStickyBitSet = ((value & S_ISVTX) == S_ISVTX); + + this.OwnerCanRead = ((value & S_IRUSR) == S_IRUSR); + + this.OwnerCanWrite = ((value & S_IWUSR) == S_IWUSR); + + this.OwnerCanExecute = ((value & S_IXUSR) == S_IXUSR); + + this.GroupCanRead = ((value & S_IRGRP) == S_IRGRP); + + this.GroupCanWrite = ((value & S_IWGRP) == S_IWGRP); + + this.GroupCanExecute = ((value & S_IXGRP) == S_IXGRP); + + this.OthersCanRead = ((value & S_IROTH) == S_IROTH); + + this.OthersCanWrite = ((value & S_IWOTH) == S_IWOTH); + + this.OthersCanExecute = ((value & S_IXOTH) == S_IXOTH); + } + } + + internal SftpFileAttributes1(DateTime lastAccessTime, DateTime lastWriteTime, long size, int userId, int groupId, uint permissions) + { + this.LastAccessTime = lastAccessTime; + this.LastWriteTime = lastWriteTime; + this.Size = size; + this.UserId = userId; + this.GroupId = groupId; + this.Permissions = permissions; + } + } +} diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpFileStream.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpFileStream.cs new file mode 100644 index 00000000..d7da96fa --- /dev/null +++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpFileStream.cs @@ -0,0 +1,1029 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.IO; +using Renci.SshNet.Sftp.Messages; + +namespace Renci.SshNet.Sftp +{ + + /// + /// Exposes a System.IO.Stream around a remote SFTP file, supporting both synchronous and asynchronous read and write operations. + /// + public class SftpFileStream : Stream + { + // TODO: Add security method to set userid, groupid and other permission settings + // Internal state. + private byte[] _handle; + private FileAccess _access; + private bool _ownsHandle; + private bool _isAsync; + private string _path; + private SftpSession _session; + + // Buffer information. + private int _bufferSize; + private byte[] _buffer; + private int _bufferPosn; + private int _bufferLen; + private long _position; + private bool _bufferOwnedByWrite; + private bool _canSeek; + + private SftpFileAttributes _attributes; + + private object _lock = new object(); + + /// + /// Gets a value indicating whether the current stream supports reading. + /// + /// true if the stream supports reading; otherwise, false. + public override bool CanRead + { + get + { + return ((this._access & FileAccess.Read) != 0); + } + } + + /// + /// Gets a value indicating whether the current stream supports seeking. + /// + /// true if the stream supports seeking; otherwise, false. + public override bool CanSeek + { + get + { + return this._canSeek; + } + } + + /// + /// Gets a value indicating whether the current stream supports writing. + /// + /// true if the stream supports writing; otherwise, false. + public override bool CanWrite + { + get + { + return ((this._access & FileAccess.Write) != 0); + } + } + + /// + /// Gets the length in bytes of the stream. + /// + /// A long value representing the length of the stream in bytes. + /// + /// A class derived from Stream does not support seeking. + /// + /// Methods were called after the stream was closed. + public override long Length + { + get + { + // Validate that the object can actually do this. + if (!this._canSeek) + { + throw new NotSupportedException("Seek operation is not supported."); + } + + // Lock down the file stream while we do this. + lock (this._lock) + { + if (this._handle == null) + { + // ECMA says this should be IOException even though + // everywhere else uses ObjectDisposedException. + throw new IOException("Stream is closed."); + } + + // Flush the write buffer, because it may + // affect the length of the stream. + if (this._bufferOwnedByWrite) + { + this.FlushWriteBuffer(); + } + + // Update file attributes + this._attributes = this._session.GetFileAttributes(this._handle); + + if (this._attributes != null && this._attributes.Size.HasValue) + { + // TODO: Test file size that greater then long.Max + return (long)this._attributes.Size.Value; + } + else + { + throw new IOException("Seek operation failed."); + } + } + } + } + + /// + /// Gets or sets the position within the current stream. + /// + /// The current position within the stream. + /// + /// An I/O error occurs. + /// + /// The stream does not support seeking. + /// + /// Methods were called after the stream was closed. + public override long Position + { + get + { + if (!this._canSeek) + { + throw new NotSupportedException("Seek operation not supported."); + } + return this._position; + } + set + { + this.Seek(value, SeekOrigin.Begin); + } + } + + /// + /// Gets a value indicating whether the FileStream was opened asynchronously or synchronously. + /// + /// + /// true if this instance is async; otherwise, false. + /// + public virtual bool IsAsync + { + get + { + return this._isAsync; + } + } + + /// + /// Gets the name of the FileStream that was passed to the constructor. + /// + public string Name { get; private set; } + + /// + /// Gets the operating system file handle for the file that the current SftpFileStream object encapsulates. + /// + public virtual byte[] Handle + { + get + { + this.Flush(); + return this._handle; + } + } + + /// + /// Gets or sets the operation timeout. + /// + /// + /// The timeout. + /// + public TimeSpan Timeout { get; set; } + + internal SftpFileStream(SftpSession session, string path, FileMode mode) + : this(session, path, mode, FileAccess.ReadWrite, 4096, false) + { + // Nothing to do here. + } + + internal SftpFileStream(SftpSession session, string path, FileMode mode, FileAccess access) + : this(session, path, mode, access, 4096, false) + { + // Nothing to do here. + } + + internal SftpFileStream(SftpSession session, string path, FileMode mode, FileAccess access, int bufferSize) + : this(session, path, mode, access, bufferSize, false) + { + // Nothing to do here. + } + + internal SftpFileStream(SftpSession session, string path, FileMode mode, FileAccess access, int bufferSize, bool useAsync) + { + // Validate the parameters. + if (path == null) + { + throw new ArgumentNullException("path"); + } + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException("bufferSize"); + } + if (access < FileAccess.Read || access > FileAccess.ReadWrite) + { + throw new ArgumentOutOfRangeException("access"); + } + if (mode < FileMode.CreateNew || mode > FileMode.Append) + { + throw new ArgumentOutOfRangeException("mode"); + } + + this.Timeout = TimeSpan.FromSeconds(30); + this.Name = path; + + // Initialize the object state. + this._session = session; + this._access = access; + this._ownsHandle = true; + this._isAsync = useAsync; + this._path = path; + this._bufferSize = bufferSize; + this._buffer = new byte[bufferSize]; + this._bufferPosn = 0; + this._bufferLen = 0; + this._bufferOwnedByWrite = false; + this._canSeek = true; + this._position = 0; + + var flags = Flags.None; + + switch (access) + { + case FileAccess.Read: + flags |= Flags.Read; + break; + case FileAccess.Write: + flags |= Flags.Write; + break; + case FileAccess.ReadWrite: + flags |= Flags.Read; + flags |= Flags.Write; + break; + default: + break; + } + + switch (mode) + { + case FileMode.Append: + flags |= Flags.Append; + break; + case FileMode.Create: + flags |= Flags.CreateNew; + flags |= Flags.Truncate; + break; + case FileMode.CreateNew: + flags |= Flags.CreateNew; + break; + case FileMode.Open: + break; + case FileMode.OpenOrCreate: + flags |= Flags.CreateNewOrOpen; + break; + case FileMode.Truncate: + flags |= Flags.Truncate; + break; + default: + break; + } + + this._handle = this._session.OpenFile(this._path, flags); + + this._attributes = this._session.GetFileAttributes(this._handle); + + if (mode == FileMode.Append) + { + // TODO: Validate Size property value exists + this._position = (long)this._attributes.Size.Value; + } + } + + internal SftpFileStream(SftpSession session, byte[] handle, FileAccess access) + : this(session, handle, access, true, 4096, false) + { + // Nothing to do here. + } + + internal SftpFileStream(SftpSession session, byte[] handle, FileAccess access, bool ownsHandle) + : this(session, handle, access, ownsHandle, 4096, false) + { + // Nothing to do here. + } + + internal SftpFileStream(SftpSession session, byte[] handle, FileAccess access, bool ownsHandle, int bufferSize) + : this(session, handle, access, ownsHandle, bufferSize, false) + { + // Nothing to do here. + } + + internal SftpFileStream(SftpSession session, byte[] handle, FileAccess access, bool ownsHandle, int bufferSize, bool isAsync) + { + // TODO: See if it make sense to have "handle" constructors + + // Validate the parameters. + if (bufferSize <= 0) + { + throw new ArgumentOutOfRangeException("bufferSize"); + } + if (access < FileAccess.Read || access > FileAccess.ReadWrite) + { + throw new ArgumentOutOfRangeException("access"); + } + + // Initialize the object state. + this._handle = handle; + this._access = access; + this._ownsHandle = ownsHandle; + this._isAsync = isAsync; + this._bufferSize = bufferSize; + this._buffer = new byte[bufferSize]; + this._bufferPosn = 0; + this._bufferLen = 0; + this._bufferOwnedByWrite = false; + this._canSeek = true; + this._position = 0; // Assumption is that no other object uses the same file handle + + this._attributes = this._session.GetFileAttributes(this._handle); + + //if (mode == FileMode.Append) + //{ + // // TODO: Validate Size property value exists + // this._position = (long)this._attributes.Size.Value; + //} + } + + /// + /// Releases unmanaged resources and performs other cleanup operations before the + /// is reclaimed by garbage collection. + /// + ~SftpFileStream() + { + this.Dispose(false); + } + + /// + /// Begins an asynchronous read operation. + /// + /// The buffer to read the data into. + /// The byte offset in at which to begin writing data read from the stream. + /// The maximum number of bytes to read. + /// An optional asynchronous callback, to be called when the read is complete. + /// A user-provided object that distinguishes this particular asynchronous read request from other requests. + /// + /// An that represents the asynchronous read, which could still be pending. + /// + /// Attempted an asynchronous read past the end of the stream, or a disk error occurs. + /// + /// One or more of the arguments is invalid. + /// + /// Methods were called after the stream was closed. + /// + /// The current Stream implementation does not support the read operation. + public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, Object state) + { + return base.BeginRead(buffer, offset, count, callback, state); + } + + /// + /// Waits for the pending asynchronous read to complete. + /// + /// The reference to the pending asynchronous request to finish. + /// + /// The number of bytes read from the stream, between zero (0) and the number of bytes you requested. Streams return zero (0) only at the end of the stream, otherwise, they should block until at least one byte is available. + /// + /// + /// is null. + /// + /// + /// did not originate from a method on the current stream. + /// + /// The stream is closed or an internal error has occurred. + public override int EndRead(IAsyncResult asyncResult) + { + return base.EndRead(asyncResult); + } + + /// + /// Begins an asynchronous write operation. + /// + /// The buffer to write data from. + /// The byte offset in from which to begin writing. + /// The maximum number of bytes to write. + /// An optional asynchronous callback, to be called when the write is complete. + /// A user-provided object that distinguishes this particular asynchronous write request from other requests. + /// + /// An IAsyncResult that represents the asynchronous write, which could still be pending. + /// + /// Attempted an asynchronous write past the end of the stream, or a disk error occurs. + /// + /// One or more of the arguments is invalid. + /// + /// Methods were called after the stream was closed. + /// + /// The current Stream implementation does not support the write operation. + public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, Object state) + { + return base.BeginWrite(buffer, offset, count, callback, state); + } + + /// + /// Ends an asynchronous write operation. + /// + /// A reference to the outstanding asynchronous I/O request. + /// + /// is null. + /// + /// + /// did not originate from a method on the current stream. + /// + /// The stream is closed or an internal error has occurred. + public override void EndWrite(IAsyncResult asyncResult) + { + base.EndWrite(asyncResult); + } + + /// + /// Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream. + /// + public override void Close() + { + this.Dispose(true); + GC.SuppressFinalize(this); + } + + /// + /// Clears all buffers for this stream and causes any buffered data to be written to the file. + /// + /// An I/O error occurs. + public override void Flush() + { + lock (this._lock) + { + if (this._handle != null) + { + if (this._bufferOwnedByWrite) + { + this.FlushWriteBuffer(); + } + else + { + this.FlushReadBuffer(); + } + } + else + { + throw new ObjectDisposedException("Stream is closed."); + } + } + } + + /// + /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. + /// + /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between and ( + - 1) replaced by the bytes read from the current source. + /// The zero-based byte offset in at which to begin storing the data read from the current stream. + /// The maximum number of bytes to be read from the current stream. + /// + /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. + /// + /// The sum of and is larger than the buffer length. + /// + /// + /// is null. + /// + /// + /// or is negative. + /// + /// An I/O error occurs. + /// + /// The stream does not support reading. + /// + /// Methods were called after the stream was closed. + public override int Read(byte[] buffer, int offset, int count) + { + int readLen = 0; + int tempLen; + + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + else if (offset < 0) + { + throw new ArgumentOutOfRangeException("offset"); + } + else if (count < 0) + { + throw new ArgumentOutOfRangeException("count"); + } + else if ((buffer.Length - offset) < count) + { + throw new ArgumentException("Invalid array range."); + } + + // Lock down the file stream while we do this. + lock (this._lock) + { + // Set up for the read operation. + this.SetupRead(); + + // Read data into the caller's buffer. + while (count > 0) + { + // How much data do we have available in the buffer? + tempLen = this._bufferLen - this._bufferPosn; + if (tempLen <= 0) + { + this._bufferPosn = 0; + + var data = this._session.Read(this._handle, (ulong)this.Position, (uint)this._bufferSize); + + this._bufferLen = data.Length; + + Buffer.BlockCopy(data, 0, this._buffer, 0, this._bufferLen); + + if (this._bufferLen < 0) + { + this._bufferLen = 0; + // TODO: Add SFTP error code or message if possible + throw new IOException("Read operation failed."); + } + else if (this._bufferLen == 0) + { + break; + } + else + { + tempLen = this._bufferLen; + } + } + + // Don't read more than the caller wants. + if (tempLen > count) + { + tempLen = count; + } + + // Copy stream data to the caller's buffer. + Array.Copy(this._buffer, this._bufferPosn, buffer, offset, tempLen); + + // Advance to the next buffer positions. + readLen += tempLen; + offset += tempLen; + count -= tempLen; + this._bufferPosn += tempLen; + this._position += tempLen; + } + } + + // Return the number of bytes that were read to the caller. + return readLen; + } + + /// + /// Reads a byte from the stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream. + /// + /// + /// The unsigned byte cast to an Int32, or -1 if at the end of the stream. + /// + /// The stream does not support reading. + /// + /// Methods were called after the stream was closed. + public override int ReadByte() + { + // Lock down the file stream while we do this. + lock (this._lock) + { + // Setup the object for reading. + this.SetupRead(); + + // Read more data into the internal buffer if necessary. + if (this._bufferPosn >= this._bufferLen) + { + this._bufferPosn = 0; + //this._bufferLen = FileMethods.Read(this._handle, this._buffer, 0, this._bufferSize); + var data = this._session.Read(this._handle, (ulong)this.Position, (uint)this._bufferSize); + this._bufferLen = data.Length; + Buffer.BlockCopy(data, 0, this._buffer, 0, this._bufferSize); + + if (this._bufferLen < 0) + { + this._bufferLen = 0; + // TODO: Add SFTP error code or message if possible + throw new IOException("Read operation failed."); + } + else if (this._bufferLen == 0) + { + // We've reached EOF. + return -1; + } + } + + // Extract the next byte from the buffer. + ++this._position; + return this._buffer[this._bufferPosn++]; + } + } + + /// + /// Sets the position within the current stream. + /// + /// A byte offset relative to the parameter. + /// A value of type indicating the reference point used to obtain the new position. + /// + /// The new position within the current stream. + /// + /// An I/O error occurs. + /// + /// The stream does not support seeking, such as if the stream is constructed from a pipe or console output. + /// + /// Methods were called after the stream was closed. + public override long Seek(long offset, SeekOrigin origin) + { + long newPosn = -1; + + // Bail out if this stream is not capable of seeking. + if (!this._canSeek) + { + throw new NotSupportedException("Seek is not supported."); + } + + // Lock down the file stream while we do this. + lock (this._lock) + { + // Bail out if the handle is invalid. + if (this._handle == null) + { + throw new ObjectDisposedException("Stream is closed."); + } + + // Don't do anything if the position won't be moving. + if (origin == SeekOrigin.Begin && offset == this._position) + { + return offset; + } + else if (origin == SeekOrigin.Current && offset == 0) + { + return this._position; + } + + this._attributes = this._session.GetFileAttributes(this._handle); + + // The behaviour depends upon the read/write mode. + if (this._bufferOwnedByWrite) + { + // Flush the write buffer and then seek. + this.FlushWriteBuffer(); + + switch (origin) + { + case SeekOrigin.Begin: + newPosn = offset; + break; + case SeekOrigin.Current: + newPosn = this._position + offset; + break; + case SeekOrigin.End: + newPosn = ((long)this._attributes.Size.Value) - offset; + break; + default: + break; + } + + if (newPosn == -1) + { + throw new EndOfStreamException("End of stream."); + } + this._position = newPosn; + } + else + { + // Determine if the seek is to somewhere inside + // the current read buffer bounds. + if (origin == SeekOrigin.Begin) + { + newPosn = this._position - this._bufferPosn; + if (offset >= newPosn && offset < + (newPosn + this._bufferLen)) + { + this._bufferPosn = (int)(offset - newPosn); + this._position = offset; + return this._position; + } + } + else if (origin == SeekOrigin.Current) + { + newPosn = this._position + offset; + if (newPosn >= (this._position - this._bufferPosn) && + newPosn < (this._position - this._bufferPosn + this._bufferLen)) + { + this._bufferPosn = + (int)(newPosn - (this._position - this._bufferPosn)); + this._position = newPosn; + return this._position; + } + } + + // Abandon the read buffer. + this._bufferPosn = 0; + this._bufferLen = 0; + + // Seek to the new position. + //newPosn = FileMethods.Seek(this._handle, offset, origin); + switch (origin) + { + case SeekOrigin.Begin: + newPosn = offset; + break; + case SeekOrigin.Current: + newPosn = this._position + offset; + break; + case SeekOrigin.End: + newPosn = ((long)this._attributes.Size.Value) - offset; + break; + default: + break; + } + + if (newPosn == -1) + { + throw new EndOfStreamException(); + } + this._position = newPosn; + } + return this._position; + } + } + + /// + /// When overridden in a derived class, sets the length of the current stream. + /// + /// The desired length of the current stream in bytes. + /// An I/O error occurs. + /// + /// The stream does not support both writing and seeking, such as if the stream is constructed from a pipe or console output. + /// + /// Methods were called after the stream was closed. + public override void SetLength(long value) + { + // Validate the parameters and setup the object for writing. + if (value < 0) + { + throw new ArgumentOutOfRangeException("value"); + } + if (!this._canSeek) + { + throw new NotSupportedException("Seek is not supported."); + } + + // Lock down the file stream while we do this. + lock (this._lock) + { + // Setup this object for writing. + this.SetupWrite(); + + this._attributes.Size = (ulong)value; + + this._session.SetFileAttributes(this._handle, this._attributes); + } + } + + /// + /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. + /// + /// An array of bytes. This method copies bytes from to the current stream. + /// The zero-based byte offset in at which to begin copying bytes to the current stream. + /// The number of bytes to be written to the current stream. + /// The sum of and is greater than the buffer length. + /// + /// + /// is null. + /// + /// + /// or is negative. + /// + /// An I/O error occurs. + /// + /// The stream does not support writing. + /// + /// Methods were called after the stream was closed. + public override void Write(byte[] buffer, int offset, int count) + { + int tempLen; + + // Validate the parameters + if (buffer == null) + { + throw new ArgumentNullException("buffer"); + } + else if (offset < 0) + { + throw new ArgumentOutOfRangeException("offset"); + } + else if (count < 0) + { + throw new ArgumentOutOfRangeException("count"); + } + else if ((buffer.Length - offset) < count) + { + throw new ArgumentException("Invalid array range."); + } + + // Lock down the file stream while we do this. + lock (this._lock) + { + // Setup this object for writing. + this.SetupWrite(); + + // Write data to the file stream. + while (count > 0) + { + // Determine how many bytes we can write to the buffer. + tempLen = this._bufferSize - this._bufferPosn; + if (tempLen <= 0) + { + var data = new byte[this._bufferPosn]; + + Buffer.BlockCopy(this._buffer, 0, data, 0, this._bufferPosn); + + this._session.Write(this._handle, (ulong)this.Position, data); + + this._bufferPosn = 0; + tempLen = this._bufferSize; + } + if (tempLen > count) + { + tempLen = count; + } + + // Can we short-cut the internal buffer? + if (this._bufferPosn == 0 && tempLen == this._bufferSize) + { + // Yes: write the data directly to the file. + var data = new byte[tempLen]; + + Buffer.BlockCopy(this._buffer, offset, data, 0, tempLen); + + this._session.Write(this._handle, (ulong)this.Position, data); + } + else + { + // No: copy the data to the write buffer first. + Array.Copy(buffer, offset, this._buffer, + this._bufferPosn, tempLen); + this._bufferPosn += tempLen; + } + + // Advance the buffer and stream positions. + this._position += tempLen; + offset += tempLen; + count -= tempLen; + } + + // If the buffer is full, then do a speculative flush now, + // rather than waiting for the next call to this method. + if (this._bufferPosn >= this._bufferSize) + { + var data = new byte[this._bufferPosn]; + + Buffer.BlockCopy(this._buffer, 0, data, 0, this._bufferPosn); + + this._session.Write(this._handle, (ulong)this.Position, data); + + this._bufferPosn = 0; + } + } + } + + /// + /// Writes a byte to the current position in the stream and advances the position within the stream by one byte. + /// + /// The byte to write to the stream. + /// An I/O error occurs. + /// + /// The stream does not support writing, or the stream is already closed. + /// + /// Methods were called after the stream was closed. + public override void WriteByte(byte value) + { + // Lock down the file stream while we do this. + lock (this._lock) + { + // Setup the object for writing. + this.SetupWrite(); + + // Flush the current buffer if it is full. + if (this._bufferPosn >= this._bufferSize) + { + var data = new byte[this._bufferPosn]; + + Buffer.BlockCopy(this._buffer, 0, data, 0, this._bufferPosn); + + this._session.Write(this._handle, (ulong)this.Position, data); + + this._bufferPosn = 0; + } + + // Write the byte into the buffer and advance the posn. + this._buffer[this._bufferPosn++] = value; + ++this._position; + } + } + + /// + /// Releases the unmanaged resources used by the and optionally releases the managed resources. + /// + /// true to release both managed and unmanaged resources; false to release only unmanaged resources. + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + + lock (this._lock) + { + if (this._handle != null) + { + if (this._bufferOwnedByWrite) + { + this.FlushWriteBuffer(); + } + + if (this._ownsHandle) + { + this._session.CloseHandle(this._handle); + } + this._handle = null; + } + } + } + + /// + /// Flushes the read data from the buffer. + /// + private void FlushReadBuffer() + { + if (this._canSeek) + { + if (this._bufferPosn < this._bufferLen) + { + this._position -= this._bufferPosn; + } + this._bufferPosn = 0; + this._bufferLen = 0; + } + } + + /// + /// Flush any buffered write data to the file. + /// + private void FlushWriteBuffer() + { + if (this._bufferPosn > 0) + { + var data = new byte[this._bufferPosn]; + + Buffer.BlockCopy(this._buffer, 0, data, 0, this._bufferPosn); + + this._session.Write(this._handle, (ulong)(this.Position - this._bufferPosn), data); + + this._bufferPosn = 0; + } + } + + /// + /// Setups the read. + /// + private void SetupRead() + { + if ((this._access & FileAccess.Read) == 0) + { + throw new NotSupportedException("Read not supported."); + } + if (this._handle == null) + { + throw new ObjectDisposedException("Stream is closed."); + } + if (this._bufferOwnedByWrite) + { + this.FlushWriteBuffer(); + this._bufferOwnedByWrite = false; + } + } + + /// + /// Setups the write. + /// + private void SetupWrite() + { + if ((this._access & FileAccess.Write) == 0) + { + throw new NotSupportedException("Write not supported."); + } + if (this._handle == null) + { + throw new ObjectDisposedException("Stream is closed."); + } + if (!this._bufferOwnedByWrite) + { + this.FlushReadBuffer(); + this._bufferOwnedByWrite = true; + } + } + } +} \ No newline at end of file diff --git a/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs b/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs index 68610edb..74ba2ac2 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/SftpSession.cs @@ -156,20 +156,69 @@ namespace Renci.SshNet.Sftp } } - /// - /// Gets the file reference from the server. - /// - /// The path. - /// - public SftpFile GetSftpFile(string path) + public byte[] OpenFile(string path, Flags flags) { - using (var cmd = new RealPathCommand(this, path)) + using (var cmd = new OpenCommand(this, path, flags)) { cmd.CommandTimeout = this._operationTimeout; cmd.Execute(); - return cmd.Files.FirstOrDefault(); + return cmd.Handle; + } + } + + public void CloseHandle(byte[] handle) + { + using (var cmd = new CloseCommand(this, handle)) + { + cmd.CommandTimeout = this._operationTimeout; + + cmd.Execute(); + } + } + + public void Write(byte[] handle, ulong offset, byte[] data) + { + using (var cmd = new WriteCommand(this, handle, offset, data)) + { + cmd.CommandTimeout = this._operationTimeout; + + cmd.Execute(); + } + } + + public byte[] Read(byte[] handle, ulong offset, uint length) + { + using (var cmd = new ReadCommand(this, handle, offset, length)) + { + cmd.CommandTimeout = this._operationTimeout; + + cmd.Execute(); + + return cmd.Data; + } + } + + public SftpFileAttributes GetFileAttributes(byte[] handle) + { + using (var cmd = new StatusCommand(this, handle)) + { + cmd.CommandTimeout = this._operationTimeout; + + cmd.Execute(); + + return cmd.Attributes; + } + } + + public void SetFileAttributes(byte[] handle, SftpFileAttributes attributes) + { + using (var cmd = new SetStatusCommand(this, handle, attributes)) + { + cmd.CommandTimeout = this._operationTimeout; + + cmd.Execute(); } } diff --git a/Renci.SshClient/Renci.SshNet/Sftp/StatusCommand.cs b/Renci.SshClient/Renci.SshNet/Sftp/StatusCommand.cs index 1043604a..f3971611 100644 --- a/Renci.SshClient/Renci.SshNet/Sftp/StatusCommand.cs +++ b/Renci.SshClient/Renci.SshNet/Sftp/StatusCommand.cs @@ -6,8 +6,9 @@ namespace Renci.SshNet.Sftp internal class StatusCommand : SftpCommand { private string _path; + private byte[] _handle; - public SftpFile File { get; private set; } + public SftpFileAttributes Attributes { get; private set; } public StatusCommand(SftpSession sftpSession, string path) : base(sftpSession) @@ -15,16 +16,29 @@ namespace Renci.SshNet.Sftp this._path = path; } + public StatusCommand(SftpSession sftpSession, byte[] handle) + : base(sftpSession) + { + this._handle = handle; + } + protected override void OnExecute() { - this.SendStatMessage(this._path); + if (this._handle != null) + { + this.SendStatMessage(this._handle); + } + else if (this._path != null) + { + this.SendStatMessage(this._path); + } } protected override void OnAttributes(SftpFileAttributes attributes) { base.OnAttributes(attributes); - this.File = new SftpFile(this.SftpSession, this._path, attributes); + this.Attributes = attributes; this.CompleteExecution(); } diff --git a/Renci.SshClient/Renci.SshNet/Sftp/WriteCommand.cs b/Renci.SshClient/Renci.SshNet/Sftp/WriteCommand.cs new file mode 100644 index 00000000..49ba355d --- /dev/null +++ b/Renci.SshClient/Renci.SshNet/Sftp/WriteCommand.cs @@ -0,0 +1,39 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Renci.SshNet.Sftp.Messages; + +namespace Renci.SshNet.Sftp +{ + internal class WriteCommand : SftpCommand + { + private byte[] _handle; + private ulong _offset; + private byte[] _data; + + public WriteCommand(SftpSession sftpSession, byte[] handle, ulong offset, byte[] data) + : base(sftpSession) + { + this._handle = handle; + this._offset = offset; + this._data = data; + } + + protected override void OnExecute() + { + this.SendWriteMessage(this._handle, this._offset, this._data); + } + + protected override void OnStatus(StatusCodes statusCode, string errorMessage, string language) + { + base.OnStatus(statusCode, errorMessage, language); + + if (statusCode == StatusCodes.Ok) + { + this.CompleteExecution(); + } + } + + } +} diff --git a/Renci.SshClient/Renci.SshNet/SftpClient.cs b/Renci.SshClient/Renci.SshNet/SftpClient.cs index 2509610a..62ebe8a9 100644 --- a/Renci.SshClient/Renci.SshNet/SftpClient.cs +++ b/Renci.SshClient/Renci.SshNet/SftpClient.cs @@ -3,6 +3,8 @@ using System.Linq; using System.Collections.Generic; using System.IO; using Renci.SshNet.Sftp; +using System.Text; +using Renci.SshNet.Common; namespace Renci.SshNet { @@ -140,7 +142,7 @@ namespace Renci.SshNet // Ensure that connection is established. this.EnsureConnection(); - var file = this._sftpSession.GetSftpFile(path); + var file = this.Get(path); file.SetPermissions(mode); } @@ -161,11 +163,12 @@ namespace Renci.SshNet var fullPath = this._sftpSession.GetCanonicalPath(path); - var cmd = new CreateDirectoryCommand(this._sftpSession, fullPath); + using (var cmd = new CreateDirectoryCommand(this._sftpSession, fullPath)) + { + cmd.CommandTimeout = this.OperationTimeout; - cmd.CommandTimeout = this.OperationTimeout; - - cmd.Execute(); + cmd.Execute(); + } } /// @@ -360,18 +363,23 @@ namespace Renci.SshNet if (path == null) throw new ArgumentNullException("path"); - // Ensure that connection is established. - this.EnsureConnection(); - var fullPath = this._sftpSession.GetCanonicalPath(path); - var cmd = new StatusCommand(this._sftpSession, fullPath); + using (var cmd = new StatusCommand(this._sftpSession, fullPath)) + { + cmd.CommandTimeout = this.OperationTimeout; - cmd.CommandTimeout = this.OperationTimeout; + cmd.Execute(); - cmd.Execute(); - - return cmd.File; + if (cmd.Attributes == null) + { + return null; + } + else + { + return new SftpFile(this._sftpSession, fullPath, cmd.Attributes); + } + } } /// @@ -540,6 +548,505 @@ namespace Renci.SshNet throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndUploadFile was called multiple times with the same IAsyncResult."); } + #region File Methods + + /// + /// Appends lines to a file, and then closes the file. + /// + /// The file to append the lines to. The file is created if it does not already exist. + /// The lines to append to the file. + public void AppendAllLines(string path, IEnumerable contents) + { + using (var stream = this.AppendText(path)) + { + foreach (var line in contents) + { + stream.WriteLine(line); + } + } + } + + /// + /// Appends lines to a file by using a specified encoding, and then closes the file. + /// + /// The file to append the lines to. The file is created if it does not already exist. + /// The lines to append to the file. + /// The character encoding to use. + public void AppendAllLines(string path, IEnumerable contents, Encoding encoding) + { + using (var stream = this.AppendText(path, encoding)) + { + foreach (var line in contents) + { + stream.WriteLine(line); + } + } + } + + /// + /// Opens a file, appends the specified string to the file, and then closes the file. + /// If the file does not exist, this method creates a file, writes the specified string to the file, then closes the file. + /// + /// The file to append the specified string to. + /// The string to append to the file. + public void AppendAllText(string path, string contents) + { + using (var stream = this.AppendText(path)) + { + stream.Write(contents); + } + } + + /// + /// Opens a file, appends the specified string to the file, and then closes the file. + /// If the file does not exist, this method creates a file, writes the specified string to the file, then closes the file. + /// + /// The file to append the specified string to. + /// The string to append to the file. + /// The character encoding to use. + public void AppendAllText(string path, string contents, Encoding encoding) + { + using (var stream = this.AppendText(path, encoding)) + { + stream.Write(contents); + } + } + + /// + /// Creates a that appends UTF-8 encoded text to an existing file. + /// + /// The path to the file to append to. + /// A StreamWriter that appends UTF-8 encoded text to an existing file. + public StreamWriter AppendText(string path) + { + return this.AppendText(path, Encoding.UTF8); + } + + /// + /// Creates a that appends UTF-8 encoded text to an existing file. + /// + /// The path to the file to append to. + /// The character encoding to use. + /// + /// A StreamWriter that appends UTF-8 encoded text to an existing file. + /// + public StreamWriter AppendText(string path, Encoding encoding) + { + return new StreamWriter(new SftpFileStream(this._sftpSession, path, FileMode.Append, FileAccess.Write), encoding); + } + + /// + /// Creates or overwrites a file in the specified path. + /// + /// The path and name of the file to create. + /// A that provides read/write access to the file specified in path + public SftpFileStream Create(string path) + { + return new SftpFileStream(this._sftpSession, path, FileMode.Create, FileAccess.ReadWrite); + } + + /// + /// Creates or overwrites the specified file. + /// + /// The path and name of the file to create. + /// The number of bytes buffered for reads and writes to the file. + /// A that provides read/write access to the file specified in path + public SftpFileStream Create(string path, int bufferSize) + { + return new SftpFileStream(this._sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize); + } + + /// + /// Creates or opens a file for writing UTF-8 encoded text. + /// + /// The file to be opened for writing. + /// A that writes to the specified file using UTF-8 encoding. + public StreamWriter CreateText(string path) + { + return new StreamWriter(this.OpenWrite(path), Encoding.UTF8); + } + + /// + /// Creates or opens a file for writing UTF-8 encoded text. + /// + /// The file to be opened for writing. + /// The character encoding to use. + /// A that writes to the specified file using UTF-8 encoding. + public StreamWriter CreateText(string path, Encoding encoding) + { + return new StreamWriter(this.OpenWrite(path), encoding); + } + + /// + /// Deletes the specified file or directory. An exception is not thrown if the specified file does not exist. + /// + /// The name of the file or directory to be deleted. Wildcard characters are not supported. + public void Delete(string path) + { + var file = this.Get(path); + + if (file == null) + { + throw new SshFileNotFoundException(path); + } + + file.Delete(); + } + + /// + /// Determines whether the specified file exists. + /// + /// The file to check. + /// true if path contains the name of an existing file; otherwise, false. + public bool Exists(string path) + { + var file = this.Get(path); + + return file != null; + } + + /// + /// Gets the System.IO.FileAttributes of the file on the path. + /// + /// The path to the file. + /// The System.IO.FileAttributes of the file on the path. + public FileAttributes GetAttributes(string path) + { + throw new NotImplementedException(); + } + + /// + /// Returns the date and time the specified file or directory was last accessed. + /// + /// The file or directory for which to obtain access date and time information. + /// A structure set to the date and time that the specified file or directory was last accessed. This value is expressed in local time. + public DateTime GetLastAccessTime(string path) + { + var file = this.Get(path); + return file.LastAccessTime; + } + + /// + /// Returns the date and time, in coordinated universal time (UTC), that the specified file or directory was last accessed. + /// + /// The file or directory for which to obtain access date and time information. + /// A structure set to the date and time that the specified file or directory was last accessed. This value is expressed in UTC time. + public DateTime GetLastAccessTimeUtc(string path) + { + var file = this.Get(path); + return file.LastAccessTime.ToUniversalTime(); + } + + /// + /// Returns the date and time the specified file or directory was last written to. + /// + /// The file or directory for which to obtain write date and time information. + /// A structure set to the date and time that the specified file or directory was last written to. This value is expressed in local time. + public DateTime GetLastWriteTime(string path) + { + var file = this.Get(path); + return file.LastWriteTime; + } + + /// + /// Returns the date and time, in coordinated universal time (UTC), that the specified file or directory was last written to. + /// + /// The file or directory for which to obtain write date and time information. + /// A structure set to the date and time that the specified file or directory was last written to. This value is expressed in UTC time. + public DateTime GetLastWriteTimeUtc(string path) + { + var file = this.Get(path); + return file.LastWriteTime.ToUniversalTime(); + } + + /// + /// Opens a on the specified path with read/write access. + /// + /// The file to open. + /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. + /// An unshared that provides access to the specified file, with the specified mode and access. + public SftpFileStream Open(string path, FileMode mode) + { + return new SftpFileStream(this._sftpSession, path, mode, FileAccess.ReadWrite); + } + + /// + /// Opens a on the specified path, with the specified mode and access. + /// + /// The file to open. + /// A value that specifies whether a file is created if one does not exist, and determines whether the contents of existing files are retained or overwritten. + /// A value that specifies the operations that can be performed on the file. + /// An unshared that provides access to the specified file, with the specified mode and access. + public SftpFileStream Open(string path, FileMode mode, FileAccess access) + { + return new SftpFileStream(this._sftpSession, path, mode, access); + } + + /// + /// Opens an existing file for reading. + /// + /// The file to be opened for reading. + /// A read-only System.IO.FileStream on the specified path. + public SftpFileStream OpenRead(string path) + { + return new SftpFileStream(this._sftpSession, path, FileMode.Open, FileAccess.Read); + } + + /// + /// Opens an existing UTF-8 encoded text file for reading. + /// + /// The file to be opened for reading. + /// A on the specified path. + public StreamReader OpenText(string path) + { + return new StreamReader(this.OpenRead(path), Encoding.UTF8); + } + + /// + /// Opens an existing file for writing. + /// + /// The file to be opened for writing. + /// An unshared object on the specified path with access. + public SftpFileStream OpenWrite(string path) + { + return new SftpFileStream(this._sftpSession, path, FileMode.OpenOrCreate, FileAccess.Write); + } + + /// + /// Opens a binary file, reads the contents of the file into a byte array, and then closes the file. + /// + /// The file to open for reading. + /// A byte array containing the contents of the file. + public byte[] ReadAllBytes(string path) + { + using (var stream = this.OpenRead(path)) + { + var buffer = new byte[stream.Length]; + stream.Read(buffer, 0, buffer.Length); + return buffer; + } + } + + /// + /// Opens a text file, reads all lines of the file, and then closes the file. + /// + /// The file to open for reading. + /// A string array containing all lines of the file. + public string[] ReadAllLines(string path) + { + return this.ReadAllLines(path, Encoding.UTF8); + } + + /// + /// Opens a file, reads all lines of the file with the specified encoding, and then closes the file. + /// + /// The file to open for reading. + /// The encoding applied to the contents of the file. + /// A string array containing all lines of the file. + public string[] ReadAllLines(string path, Encoding encoding) + { + var lines = new List(); + using (var stream = new StreamReader(this.OpenRead(path), encoding)) + { + while (!stream.EndOfStream) + { + lines.Add(stream.ReadLine()); + } + } + return lines.ToArray(); + } + + /// + /// Opens a text file, reads all lines of the file, and then closes the file. + /// + /// The file to open for reading. + /// A string containing all lines of the file. + public string ReadAllText(string path) + { + return this.ReadAllText(path, Encoding.UTF8); + } + + /// + /// Opens a file, reads all lines of the file with the specified encoding, and then closes the file. + /// + /// The file to open for reading. + /// The encoding applied to the contents of the file. + /// A string containing all lines of the file. + public string ReadAllText(string path, Encoding encoding) + { + var lines = new List(); + using (var stream = new StreamReader(this.OpenRead(path), encoding)) + { + return stream.ReadToEnd(); + } + } + + /// + /// Reads the lines of a file. + /// + /// The file to read. + /// The lines of the file. + public IEnumerable ReadLines(string path) + { + return this.ReadAllLines(path); + } + + /// + /// Read the lines of a file that has a specified encoding. + /// + /// The file to read. + /// The encoding that is applied to the contents of the file. + /// The lines of the file. + public IEnumerable ReadLines(string path, Encoding encoding) + { + return this.ReadAllLines(path, encoding); + } + + /// + /// Sets the date and time the specified file was last accessed. + /// + /// The file for which to set the access date and time information. + /// A containing the value to set for the last access date and time of path. This value is expressed in local time. + public void SetLastAccessTime(string path, DateTime lastAccessTime) + { + throw new NotImplementedException(); + } + + /// + /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last accessed. + /// + /// The file for which to set the access date and time information. + /// A containing the value to set for the last access date and time of path. This value is expressed in UTC time. + public void SetLastAccessTimeUtc(string path, DateTime lastAccessTimeUtc) + { + throw new NotImplementedException(); + } + + /// + /// Sets the date and time that the specified file was last written to. + /// + /// The file for which to set the date and time information. + /// A System.DateTime containing the value to set for the last write date and time of path. This value is expressed in local time. + public void SetLastWriteTime(string path, DateTime lastWriteTime) + { + throw new NotImplementedException(); + } + + /// + /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last written to. + /// + /// The file for which to set the date and time information. + /// A System.DateTime containing the value to set for the last write date and time of path. This value is expressed in UTC time. + public void SetLastWriteTimeUtc(string path, DateTime lastWriteTimeUtc) + { + throw new NotImplementedException(); + } + + /// + /// Creates a new file, writes the specified byte array to the file, and then closes the file. If the target file already exists, it is overwritten. + /// + /// The file to write to. + /// The bytes to write to the file. + public void WriteAllBytes(string path, byte[] bytes) + { + using (var stream = this.OpenWrite(path)) + { + stream.Write(bytes, 0, bytes.Length); + } + } + + /// + /// Creates a new file, writes a collection of strings to the file, and then closes the file. + /// + /// The file to write to. + /// The lines to write to the file. + public void WriteAllLines(string path, IEnumerable contents) + { + this.WriteAllLines(path, contents, Encoding.UTF8); + } + + /// + /// Creates a new file, write the specified string array to the file, and then closes the file. + /// + /// The file to write to. + /// The string array to write to the file. + public void WriteAllLines(string path, string[] contents) + { + this.WriteAllLines(path, contents, Encoding.UTF8); + } + + /// + /// Creates a new file by using the specified encoding, writes a collection of strings to the file, and then closes the file. + /// + /// The file to write to. + /// The lines to write to the file. + /// The character encoding to use. + public void WriteAllLines(string path, IEnumerable contents, Encoding encoding) + { + using (var stream = this.CreateText(path, encoding)) + { + foreach (var line in contents) + { + stream.WriteLine(line); + } + } + } + + /// + /// Creates a new file, writes the specified string array to the file by using the specified encoding, and then closes the file. + /// + /// The file to write to. + /// The string array to write to the file. + /// An object that represents the character encoding applied to the string array. + public void WriteAllLines(string path, string[] contents, Encoding encoding) + { + using (var stream = this.CreateText(path, encoding)) + { + foreach (var line in contents) + { + stream.WriteLine(line); + } + } + } + + /// + /// Creates a new file, writes the specified string to the file, and then closes the file. If the target file already exists, it is overwritten. + /// + /// The file to write to. + /// The string to write to the file. + public void WriteAllText(string path, string contents) + { + using (var stream = this.CreateText(path)) + { + stream.Write(contents); + } + } + + /// + /// Creates a new file, writes the specified string to the file using the specified encoding, and then closes the file. If the target file already exists, it is overwritten. + /// + /// The file to write to. + /// The string to write to the file. + /// The encoding to apply to the string. + public void WriteAllText(string path, string contents, Encoding encoding) + { + using (var stream = this.CreateText(path, encoding)) + { + stream.Write(contents); + } + } + + //public FileSecurity GetAccessControl(string path); + //public FileSecurity GetAccessControl(string path, AccessControlSections includeSections); + //public DateTime GetCreationTime(string path); + //public DateTime GetCreationTimeUtc(string path); + //public void SetAccessControl(string path, FileSecurity fileSecurity); + //public void SetAttributes(string path, FileAttributes fileAttributes); + //public void SetCreationTime(string path, DateTime creationTime); + //public void SetCreationTimeUtc(string path, DateTime creationTimeUtc); + + #endregion + /// /// Called when client is connected to the server. ///