using System; using System.IO; using System.Text.RegularExpressions; using Renci.SshNet.Channels; using Renci.SshNet.Common; namespace Renci.SshNet { /// /// Provides SCP client functionality. /// public partial class ScpClient { private static readonly Regex DirectoryInfoRe = new Regex(@"D(?\d{4}) (?\d+) (?.+)"); private static readonly Regex TimestampRe = new Regex(@"T(?\d+) 0 (?\d+) 0"); /// /// Uploads the specified file to the remote host. /// /// The file system info. /// A relative or absolute path for the remote file. /// is null. /// is null. /// is a zero-length . /// A directory with the specified path exists on the remote host. /// The secure copy execution request was rejected by the server. public void Upload(FileInfo fileInfo, string path) { if (fileInfo is null) { throw new ArgumentNullException(nameof(fileInfo)); } var posixPath = PosixPath.CreateAbsoluteOrRelativeFilePath(path); using (var input = ServiceFactory.CreatePipeStream()) using (var channel = Session.CreateChannelSession()) { channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length); channel.Open(); // Pass only the directory part of the path to the server, and use the (hidden) -d option to signal // that we expect the target to be a directory. if (!channel.SendExecRequest(string.Format("scp -t -d {0}", _remotePathTransformation.Transform(posixPath.Directory)))) { throw SecureExecutionRequestRejectedException(); } CheckReturnCode(input); using (var source = fileInfo.OpenRead()) { UploadTimes(channel, input, fileInfo); UploadFileModeAndName(channel, input, source.Length, posixPath.File); UploadFileContent(channel, input, source, fileInfo.Name); } } } /// /// Uploads the specified directory to the remote host. /// /// The directory info. /// A relative or absolute path for the remote directory. /// is null. /// is null. /// is a zero-length string. /// does not exist on the remote host, is not a directory or the user does not have the required permission. /// The secure copy execution request was rejected by the server. public void Upload(DirectoryInfo directoryInfo, string path) { if (directoryInfo is null) { throw new ArgumentNullException(nameof(directoryInfo)); } if (path is null) { throw new ArgumentNullException(nameof(path)); } if (path.Length == 0) { throw new ArgumentException("The path cannot be a zero-length string.", nameof(path)); } using (var input = ServiceFactory.CreatePipeStream()) using (var channel = Session.CreateChannelSession()) { channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length); channel.Open(); // start copy with the following options: // -p preserve modification and access times // -r copy directories recursively // -d expect path to be a directory // -t copy to remote if (!channel.SendExecRequest(string.Format("scp -r -p -d -t {0}", _remotePathTransformation.Transform(path)))) { throw SecureExecutionRequestRejectedException(); } CheckReturnCode(input); UploadDirectoryContent(channel, input, directoryInfo); } } /// /// Downloads the specified file from the remote host to local file. /// /// Remote host file name. /// Local file information. /// is null. /// is null or empty. /// exists on the remote host, and is not a regular file. /// The secure copy execution request was rejected by the server. public void Download(string filename, FileInfo fileInfo) { if (string.IsNullOrEmpty(filename)) { throw new ArgumentException("filename"); } if (fileInfo is null) { throw new ArgumentNullException(nameof(fileInfo)); } using (var input = ServiceFactory.CreatePipeStream()) using (var channel = Session.CreateChannelSession()) { channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length); channel.Open(); // Send channel command request if (!channel.SendExecRequest(string.Format("scp -pf {0}", _remotePathTransformation.Transform(filename)))) { throw SecureExecutionRequestRejectedException(); } // Send reply SendSuccessConfirmation(channel); InternalDownload(channel, input, fileInfo); } } /// /// Downloads the specified directory from the remote host to local directory. /// /// Remote host directory name. /// Local directory information. /// is null or empty. /// is null. /// File or directory with the specified path does not exist on the remote host. /// The secure copy execution request was rejected by the server. public void Download(string directoryName, DirectoryInfo directoryInfo) { if (string.IsNullOrEmpty(directoryName)) { throw new ArgumentException("directoryName"); } if (directoryInfo is null) { throw new ArgumentNullException(nameof(directoryInfo)); } using (var input = ServiceFactory.CreatePipeStream()) using (var channel = Session.CreateChannelSession()) { channel.DataReceived += (sender, e) => input.Write(e.Data, 0, e.Data.Length); channel.Open(); // Send channel command request if (!channel.SendExecRequest(string.Format("scp -prf {0}", _remotePathTransformation.Transform(directoryName)))) { throw SecureExecutionRequestRejectedException(); } // Send reply SendSuccessConfirmation(channel); InternalDownload(channel, input, directoryInfo); } } /// /// Uploads the and /// of the next file or directory to upload. /// /// The channel to perform the upload in. /// A from which any feedback from the server can be read. /// The file or directory to upload. private void UploadTimes(IChannelSession channel, Stream input, FileSystemInfo fileOrDirectory) { var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); var modificationSeconds = (long) (fileOrDirectory.LastWriteTimeUtc - zeroTime).TotalSeconds; var accessSeconds = (long) (fileOrDirectory.LastAccessTimeUtc - zeroTime).TotalSeconds; SendData(channel, string.Format("T{0} 0 {1} 0\n", modificationSeconds, accessSeconds)); CheckReturnCode(input); } /// /// Upload the files and subdirectories in the specified directory. /// /// The channel to perform the upload in. /// A from which any feedback from the server can be read. /// The directory to upload. private void UploadDirectoryContent(IChannelSession channel, Stream input, DirectoryInfo directoryInfo) { // Upload files var files = directoryInfo.GetFiles(); foreach (var file in files) { using (var source = file.OpenRead()) { UploadTimes(channel, input, file); UploadFileModeAndName(channel, input, source.Length, file.Name); UploadFileContent(channel, input, source, file.Name); } } // Upload directories var directories = directoryInfo.GetDirectories(); foreach (var directory in directories) { UploadTimes(channel, input, directory); UploadDirectoryModeAndName(channel, input, directory.Name); UploadDirectoryContent(channel, input, directory); } // Mark upload of current directory complete SendData(channel, "E\n"); CheckReturnCode(input); } /// /// Sets mode and name of the directory being upload. /// private void UploadDirectoryModeAndName(IChannelSession channel, Stream input, string directoryName) { SendData(channel, string.Format("D0755 0 {0}\n", directoryName)); CheckReturnCode(input); } private void InternalDownload(IChannelSession channel, Stream input, FileSystemInfo fileSystemInfo) { var modifiedTime = DateTime.Now; var accessedTime = DateTime.Now; var startDirectoryFullName = fileSystemInfo.FullName; var currentDirectoryFullName = startDirectoryFullName; var directoryCounter = 0; while (true) { var message = ReadString(input); if (message == "E") { SendSuccessConfirmation(channel); // Send reply directoryCounter--; currentDirectoryFullName = new DirectoryInfo(currentDirectoryFullName).Parent.FullName; if (directoryCounter == 0) { break; } continue; } var match = DirectoryInfoRe.Match(message); if (match.Success) { SendSuccessConfirmation(channel); // Send reply // Read directory var filename = match.Result("${filename}"); DirectoryInfo newDirectoryInfo; if (directoryCounter > 0) { newDirectoryInfo = Directory.CreateDirectory(Path.Combine(currentDirectoryFullName, filename)); newDirectoryInfo.LastAccessTime = accessedTime; newDirectoryInfo.LastWriteTime = modifiedTime; } else { // Don't create directory for first level newDirectoryInfo = fileSystemInfo as DirectoryInfo; } directoryCounter++; currentDirectoryFullName = newDirectoryInfo.FullName; continue; } match = FileInfoRe.Match(message); if (match.Success) { // Read file SendSuccessConfirmation(channel); // Send reply var length = long.Parse(match.Result("${length}")); var fileName = match.Result("${filename}"); if (fileSystemInfo is not FileInfo fileInfo) { fileInfo = new FileInfo(Path.Combine(currentDirectoryFullName, fileName)); } using (var output = fileInfo.OpenWrite()) { InternalDownload(channel, input, output, fileName, length); } fileInfo.LastAccessTime = accessedTime; fileInfo.LastWriteTime = modifiedTime; if (directoryCounter == 0) { break; } continue; } match = TimestampRe.Match(message); if (match.Success) { // Read timestamp SendSuccessConfirmation(channel); // Send reply var mtime = long.Parse(match.Result("${mtime}")); var atime = long.Parse(match.Result("${atime}")); var zeroTime = new DateTime(1970, 1, 1, 0, 0, 0, 0, DateTimeKind.Utc); modifiedTime = zeroTime.AddSeconds(mtime); accessedTime = zeroTime.AddSeconds(atime); continue; } SendErrorConfirmation(channel, string.Format("\"{0}\" is not valid protocol message.", message)); } } } }