SftpClient.cs 131 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569
  1. #nullable enable
  2. using System;
  3. using System.Collections.Generic;
  4. using System.Diagnostics.CodeAnalysis;
  5. using System.Globalization;
  6. using System.IO;
  7. using System.Net;
  8. using System.Runtime.CompilerServices;
  9. using System.Text;
  10. using System.Threading;
  11. using System.Threading.Tasks;
  12. using Renci.SshNet.Abstractions;
  13. using Renci.SshNet.Common;
  14. using Renci.SshNet.Sftp;
  15. namespace Renci.SshNet
  16. {
  17. /// <summary>
  18. /// Implementation of the SSH File Transfer Protocol (SFTP) over SSH.
  19. /// </summary>
  20. public class SftpClient : BaseClient, ISftpClient
  21. {
  22. private static readonly Encoding Utf8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true);
  23. /// <summary>
  24. /// Holds the <see cref="ISftpSession"/> instance that is used to communicate to the
  25. /// SFTP server.
  26. /// </summary>
  27. private ISftpSession? _sftpSession;
  28. /// <summary>
  29. /// Holds the operation timeout.
  30. /// </summary>
  31. private int _operationTimeout;
  32. /// <summary>
  33. /// Holds the size of the buffer.
  34. /// </summary>
  35. private uint _bufferSize;
  36. /// <summary>
  37. /// Gets or sets the operation timeout.
  38. /// </summary>
  39. /// <value>
  40. /// The timeout to wait until an operation completes. The default value is negative
  41. /// one (-1) milliseconds, which indicates an infinite timeout period.
  42. /// </value>
  43. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  44. /// <exception cref="ArgumentOutOfRangeException"><paramref name="value"/> represents a value that is less than -1 or greater than <see cref="int.MaxValue"/> milliseconds.</exception>
  45. public TimeSpan OperationTimeout
  46. {
  47. get
  48. {
  49. CheckDisposed();
  50. return TimeSpan.FromMilliseconds(_operationTimeout);
  51. }
  52. set
  53. {
  54. CheckDisposed();
  55. _operationTimeout = value.AsTimeout(nameof(OperationTimeout));
  56. }
  57. }
  58. /// <summary>
  59. /// Gets or sets the maximum size of the buffer in bytes.
  60. /// </summary>
  61. /// <value>
  62. /// The size of the buffer. The default buffer size is 32768 bytes (32 KB).
  63. /// </value>
  64. /// <remarks>
  65. /// <para>
  66. /// For write operations, this limits the size of the payload for
  67. /// individual SSH_FXP_WRITE messages. The actual size is always
  68. /// capped at the maximum packet size supported by the peer
  69. /// (minus the size of protocol fields).
  70. /// </para>
  71. /// <para>
  72. /// For read operations, this controls the size of the payload which
  73. /// is requested from the peer in a SSH_FXP_READ message. The peer
  74. /// will send the requested number of bytes in a SSH_FXP_DATA message,
  75. /// possibly split over multiple SSH_MSG_CHANNEL_DATA messages.
  76. /// </para>
  77. /// <para>
  78. /// To optimize the size of the SSH packets sent by the peer,
  79. /// the actual requested size will take into account the size of the
  80. /// SSH_FXP_DATA protocol fields.
  81. /// </para>
  82. /// <para>
  83. /// The size of the each individual SSH_FXP_DATA message is limited to the
  84. /// local maximum packet size of the channel, which is set to <c>64 KB</c>
  85. /// for SSH.NET. However, the peer can limit this even further.
  86. /// </para>
  87. /// </remarks>
  88. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  89. public uint BufferSize
  90. {
  91. get
  92. {
  93. CheckDisposed();
  94. return _bufferSize;
  95. }
  96. set
  97. {
  98. CheckDisposed();
  99. _bufferSize = value;
  100. }
  101. }
  102. /// <summary>
  103. /// Gets a value indicating whether this client is connected to the server and
  104. /// the SFTP session is open.
  105. /// </summary>
  106. /// <value>
  107. /// <see langword="true"/> if this client is connected and the SFTP session is open; otherwise, <see langword="false"/>.
  108. /// </value>
  109. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  110. public override bool IsConnected
  111. {
  112. get
  113. {
  114. return base.IsConnected && _sftpSession?.IsOpen == true;
  115. }
  116. }
  117. /// <summary>
  118. /// Gets remote working directory.
  119. /// </summary>
  120. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  121. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  122. public string WorkingDirectory
  123. {
  124. get
  125. {
  126. CheckDisposed();
  127. if (_sftpSession is null)
  128. {
  129. throw new SshConnectionException("Client not connected.");
  130. }
  131. return _sftpSession.WorkingDirectory;
  132. }
  133. }
  134. /// <summary>
  135. /// Gets sftp protocol version.
  136. /// </summary>
  137. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  138. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  139. public int ProtocolVersion
  140. {
  141. get
  142. {
  143. CheckDisposed();
  144. if (_sftpSession is null)
  145. {
  146. throw new SshConnectionException("Client not connected.");
  147. }
  148. return (int)_sftpSession.ProtocolVersion;
  149. }
  150. }
  151. /// <summary>
  152. /// Gets the current SFTP session.
  153. /// </summary>
  154. /// <value>
  155. /// The current SFTP session.
  156. /// </value>
  157. internal ISftpSession? SftpSession
  158. {
  159. get { return _sftpSession; }
  160. }
  161. #region Constructors
  162. /// <summary>
  163. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  164. /// </summary>
  165. /// <param name="connectionInfo">The connection info.</param>
  166. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  167. public SftpClient(ConnectionInfo connectionInfo)
  168. : this(connectionInfo, ownsConnectionInfo: false)
  169. {
  170. }
  171. /// <summary>
  172. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  173. /// </summary>
  174. /// <param name="host">Connection host.</param>
  175. /// <param name="port">Connection port.</param>
  176. /// <param name="username">Authentication username.</param>
  177. /// <param name="password">Authentication password.</param>
  178. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
  179. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid. <para>-or-</para> <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  180. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  181. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  182. public SftpClient(string host, int port, string username, string password)
  183. : this(new PasswordConnectionInfo(host, port, username, password), ownsConnectionInfo: true)
  184. {
  185. }
  186. /// <summary>
  187. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  188. /// </summary>
  189. /// <param name="host">Connection host.</param>
  190. /// <param name="username">Authentication username.</param>
  191. /// <param name="password">Authentication password.</param>
  192. /// <exception cref="ArgumentNullException"><paramref name="password"/> is <see langword="null"/>.</exception>
  193. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid. <para>-or-</para> <paramref name="username"/> is <see langword="null"/> contains only whitespace characters.</exception>
  194. public SftpClient(string host, string username, string password)
  195. : this(host, ConnectionInfo.DefaultPort, username, password)
  196. {
  197. }
  198. /// <summary>
  199. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  200. /// </summary>
  201. /// <param name="host">Connection host.</param>
  202. /// <param name="port">Connection port.</param>
  203. /// <param name="username">Authentication username.</param>
  204. /// <param name="keyFiles">Authentication private key file(s) .</param>
  205. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
  206. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid. <para>-or-</para> <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  207. /// <exception cref="ArgumentOutOfRangeException"><paramref name="port"/> is not within <see cref="IPEndPoint.MinPort"/> and <see cref="IPEndPoint.MaxPort"/>.</exception>
  208. [SuppressMessage("Microsoft.Reliability", "CA2000:DisposeObjectsBeforeLosingScope", Justification = "Disposed in Dispose(bool) method.")]
  209. public SftpClient(string host, int port, string username, params IPrivateKeySource[] keyFiles)
  210. : this(new PrivateKeyConnectionInfo(host, port, username, keyFiles), ownsConnectionInfo: true)
  211. {
  212. }
  213. /// <summary>
  214. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  215. /// </summary>
  216. /// <param name="host">Connection host.</param>
  217. /// <param name="username">Authentication username.</param>
  218. /// <param name="keyFiles">Authentication private key file(s) .</param>
  219. /// <exception cref="ArgumentNullException"><paramref name="keyFiles"/> is <see langword="null"/>.</exception>
  220. /// <exception cref="ArgumentException"><paramref name="host"/> is invalid. <para>-or-</para> <paramref name="username"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  221. public SftpClient(string host, string username, params IPrivateKeySource[] keyFiles)
  222. : this(host, ConnectionInfo.DefaultPort, username, keyFiles)
  223. {
  224. }
  225. /// <summary>
  226. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  227. /// </summary>
  228. /// <param name="connectionInfo">The connection info.</param>
  229. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  230. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  231. /// <remarks>
  232. /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, the connection info will be disposed when this
  233. /// instance is disposed.
  234. /// </remarks>
  235. private SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo)
  236. : this(connectionInfo, ownsConnectionInfo, new ServiceFactory())
  237. {
  238. }
  239. /// <summary>
  240. /// Initializes a new instance of the <see cref="SftpClient"/> class.
  241. /// </summary>
  242. /// <param name="connectionInfo">The connection info.</param>
  243. /// <param name="ownsConnectionInfo">Specified whether this instance owns the connection info.</param>
  244. /// <param name="serviceFactory">The factory to use for creating new services.</param>
  245. /// <exception cref="ArgumentNullException"><paramref name="connectionInfo"/> is <see langword="null"/>.</exception>
  246. /// <exception cref="ArgumentNullException"><paramref name="serviceFactory"/> is <see langword="null"/>.</exception>
  247. /// <remarks>
  248. /// If <paramref name="ownsConnectionInfo"/> is <see langword="true"/>, the connection info will be disposed when this
  249. /// instance is disposed.
  250. /// </remarks>
  251. internal SftpClient(ConnectionInfo connectionInfo, bool ownsConnectionInfo, IServiceFactory serviceFactory)
  252. : base(connectionInfo, ownsConnectionInfo, serviceFactory)
  253. {
  254. _operationTimeout = Timeout.Infinite;
  255. _bufferSize = 1024 * 32;
  256. }
  257. #endregion Constructors
  258. /// <summary>
  259. /// Changes remote directory to path.
  260. /// </summary>
  261. /// <param name="path">New directory path.</param>
  262. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  263. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  264. /// <exception cref="SftpPermissionDeniedException">Permission to change directory denied by remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  265. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  266. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  267. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  268. public void ChangeDirectory(string path)
  269. {
  270. CheckDisposed();
  271. ThrowHelper.ThrowIfNull(path);
  272. if (_sftpSession is null)
  273. {
  274. throw new SshConnectionException("Client not connected.");
  275. }
  276. _sftpSession.ChangeDirectory(path);
  277. }
  278. /// <summary>
  279. /// Asynchronously requests to change the current working directory to the specified path.
  280. /// </summary>
  281. /// <param name="path">The new working directory.</param>
  282. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  283. /// <returns>A <see cref="Task"/> that tracks the asynchronous change working directory request.</returns>
  284. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  285. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  286. /// <exception cref="SftpPermissionDeniedException">Permission to change directory denied by remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  287. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  288. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  289. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  290. public Task ChangeDirectoryAsync(string path, CancellationToken cancellationToken = default)
  291. {
  292. CheckDisposed();
  293. ThrowHelper.ThrowIfNull(path);
  294. if (_sftpSession is null)
  295. {
  296. throw new SshConnectionException("Client not connected.");
  297. }
  298. cancellationToken.ThrowIfCancellationRequested();
  299. return _sftpSession.ChangeDirectoryAsync(path, cancellationToken);
  300. }
  301. /// <summary>
  302. /// Changes permissions of file(s) to specified mode.
  303. /// </summary>
  304. /// <param name="path">File(s) path, may match multiple files.</param>
  305. /// <param name="mode">The mode.</param>
  306. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  307. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  308. /// <exception cref="SftpPermissionDeniedException">Permission to change permission on the path(s) was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  309. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  310. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  311. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  312. public void ChangePermissions(string path, short mode)
  313. {
  314. var file = Get(path);
  315. file.SetPermissions(mode);
  316. }
  317. /// <summary>
  318. /// Creates remote directory specified by path.
  319. /// </summary>
  320. /// <param name="path">Directory path to create.</param>
  321. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  322. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  323. /// <exception cref="SftpPermissionDeniedException">Permission to create the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  324. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  325. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  326. public void CreateDirectory(string path)
  327. {
  328. CheckDisposed();
  329. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  330. if (_sftpSession is null)
  331. {
  332. throw new SshConnectionException("Client not connected.");
  333. }
  334. var fullPath = _sftpSession.GetCanonicalPath(path);
  335. _sftpSession.RequestMkDir(fullPath);
  336. }
  337. /// <summary>
  338. /// Asynchronously requests to create a remote directory specified by path.
  339. /// </summary>
  340. /// <param name="path">Directory path to create.</param>
  341. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  342. /// <returns>A <see cref="Task"/> that represents the asynchronous create directory operation.</returns>
  343. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  344. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  345. /// <exception cref="SftpPermissionDeniedException">Permission to create the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  346. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  347. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  348. public async Task CreateDirectoryAsync(string path, CancellationToken cancellationToken = default)
  349. {
  350. CheckDisposed();
  351. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  352. if (_sftpSession is null)
  353. {
  354. throw new SshConnectionException("Client not connected.");
  355. }
  356. var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
  357. await _sftpSession.RequestMkDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
  358. }
  359. /// <summary>
  360. /// Deletes remote directory specified by path.
  361. /// </summary>
  362. /// <param name="path">Directory to be deleted path.</param>
  363. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  364. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  365. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  366. /// <exception cref="SftpPermissionDeniedException">Permission to delete the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  367. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  368. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  369. public void DeleteDirectory(string path)
  370. {
  371. CheckDisposed();
  372. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  373. if (_sftpSession is null)
  374. {
  375. throw new SshConnectionException("Client not connected.");
  376. }
  377. var fullPath = _sftpSession.GetCanonicalPath(path);
  378. _sftpSession.RequestRmDir(fullPath);
  379. }
  380. /// <summary>
  381. /// Deletes remote file specified by path.
  382. /// </summary>
  383. /// <param name="path">File to be deleted path.</param>
  384. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  385. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  386. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  387. /// <exception cref="SftpPermissionDeniedException">Permission to delete the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  388. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  389. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  390. public void DeleteFile(string path)
  391. {
  392. CheckDisposed();
  393. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  394. if (_sftpSession is null)
  395. {
  396. throw new SshConnectionException("Client not connected.");
  397. }
  398. var fullPath = _sftpSession.GetCanonicalPath(path);
  399. _sftpSession.RequestRemove(fullPath);
  400. }
  401. /// <summary>
  402. /// Asynchronously deletes remote file specified by path.
  403. /// </summary>
  404. /// <param name="path">File to be deleted path.</param>
  405. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  406. /// <returns>A <see cref="Task"/> that represents the asynchronous delete operation.</returns>
  407. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  408. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  409. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  410. /// <exception cref="SftpPermissionDeniedException">Permission to delete the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  411. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  412. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  413. public async Task DeleteFileAsync(string path, CancellationToken cancellationToken)
  414. {
  415. CheckDisposed();
  416. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  417. if (_sftpSession is null)
  418. {
  419. throw new SshConnectionException("Client not connected.");
  420. }
  421. cancellationToken.ThrowIfCancellationRequested();
  422. var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
  423. await _sftpSession.RequestRemoveAsync(fullPath, cancellationToken).ConfigureAwait(false);
  424. }
  425. /// <summary>
  426. /// Renames remote file from old path to new path.
  427. /// </summary>
  428. /// <param name="oldPath">Path to the old file location.</param>
  429. /// <param name="newPath">Path to the new file location.</param>
  430. /// <exception cref="ArgumentNullException"><paramref name="oldPath"/> is <see langword="null"/>. <para>-or-</para> or <paramref name="newPath"/> is <see langword="null"/>.</exception>
  431. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  432. /// <exception cref="SftpPermissionDeniedException">Permission to rename the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  433. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  434. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  435. public void RenameFile(string oldPath, string newPath)
  436. {
  437. RenameFile(oldPath, newPath, isPosix: false);
  438. }
  439. /// <summary>
  440. /// Renames remote file from old path to new path.
  441. /// </summary>
  442. /// <param name="oldPath">Path to the old file location.</param>
  443. /// <param name="newPath">Path to the new file location.</param>
  444. /// <param name="isPosix">if set to <see langword="true"/> then perform a posix rename.</param>
  445. /// <exception cref="ArgumentNullException"><paramref name="oldPath" /> is <see langword="null"/>. <para>-or-</para> or <paramref name="newPath" /> is <see langword="null"/>.</exception>
  446. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  447. /// <exception cref="SftpPermissionDeniedException">Permission to rename the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  448. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  449. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  450. public void RenameFile(string oldPath, string newPath, bool isPosix)
  451. {
  452. CheckDisposed();
  453. ThrowHelper.ThrowIfNull(oldPath);
  454. ThrowHelper.ThrowIfNull(newPath);
  455. if (_sftpSession is null)
  456. {
  457. throw new SshConnectionException("Client not connected.");
  458. }
  459. var oldFullPath = _sftpSession.GetCanonicalPath(oldPath);
  460. var newFullPath = _sftpSession.GetCanonicalPath(newPath);
  461. if (isPosix)
  462. {
  463. _sftpSession.RequestPosixRename(oldFullPath, newFullPath);
  464. }
  465. else
  466. {
  467. _sftpSession.RequestRename(oldFullPath, newFullPath);
  468. }
  469. }
  470. /// <summary>
  471. /// Asynchronously renames remote file from old path to new path.
  472. /// </summary>
  473. /// <param name="oldPath">Path to the old file location.</param>
  474. /// <param name="newPath">Path to the new file location.</param>
  475. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  476. /// <returns>A <see cref="Task"/> that represents the asynchronous rename operation.</returns>
  477. /// <exception cref="ArgumentNullException"><paramref name="oldPath"/> is <see langword="null"/>. <para>-or-</para> or <paramref name="newPath"/> is <see langword="null"/>.</exception>
  478. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  479. /// <exception cref="SftpPermissionDeniedException">Permission to rename the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  480. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  481. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  482. public async Task RenameFileAsync(string oldPath, string newPath, CancellationToken cancellationToken)
  483. {
  484. CheckDisposed();
  485. ThrowHelper.ThrowIfNull(oldPath);
  486. ThrowHelper.ThrowIfNull(newPath);
  487. if (_sftpSession is null)
  488. {
  489. throw new SshConnectionException("Client not connected.");
  490. }
  491. cancellationToken.ThrowIfCancellationRequested();
  492. var oldFullPath = await _sftpSession.GetCanonicalPathAsync(oldPath, cancellationToken).ConfigureAwait(false);
  493. var newFullPath = await _sftpSession.GetCanonicalPathAsync(newPath, cancellationToken).ConfigureAwait(false);
  494. await _sftpSession.RequestRenameAsync(oldFullPath, newFullPath, cancellationToken).ConfigureAwait(false);
  495. }
  496. /// <summary>
  497. /// Creates a symbolic link from old path to new path.
  498. /// </summary>
  499. /// <param name="path">The old path.</param>
  500. /// <param name="linkPath">The new path.</param>
  501. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/>. <para>-or-</para> <paramref name="linkPath"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  502. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  503. /// <exception cref="SftpPermissionDeniedException">Permission to create the symbolic link was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  504. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  505. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  506. public void SymbolicLink(string path, string linkPath)
  507. {
  508. CheckDisposed();
  509. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  510. ThrowHelper.ThrowIfNullOrWhiteSpace(linkPath);
  511. if (_sftpSession is null)
  512. {
  513. throw new SshConnectionException("Client not connected.");
  514. }
  515. var fullPath = _sftpSession.GetCanonicalPath(path);
  516. var linkFullPath = _sftpSession.GetCanonicalPath(linkPath);
  517. _sftpSession.RequestSymLink(fullPath, linkFullPath);
  518. }
  519. /// <summary>
  520. /// Retrieves list of files in remote directory.
  521. /// </summary>
  522. /// <param name="path">The path.</param>
  523. /// <param name="listCallback">The list callback.</param>
  524. /// <returns>
  525. /// A list of files.
  526. /// </returns>
  527. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  528. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  529. /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  530. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  531. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  532. public IEnumerable<ISftpFile> ListDirectory(string path, Action<int>? listCallback = null)
  533. {
  534. CheckDisposed();
  535. return InternalListDirectory(path, asyncResult: null, listCallback);
  536. }
  537. /// <summary>
  538. /// Asynchronously enumerates the files in remote directory.
  539. /// </summary>
  540. /// <param name="path">The path.</param>
  541. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  542. /// <returns>
  543. /// An <see cref="IAsyncEnumerable{T}"/> of <see cref="ISftpFile"/> that represents the asynchronous enumeration operation.
  544. /// The enumeration contains an async stream of <see cref="ISftpFile"/> for the files in the directory specified by <paramref name="path" />.
  545. /// </returns>
  546. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  547. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  548. /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  549. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  550. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  551. public async IAsyncEnumerable<ISftpFile> ListDirectoryAsync(string path, [EnumeratorCancellation] CancellationToken cancellationToken)
  552. {
  553. CheckDisposed();
  554. ThrowHelper.ThrowIfNull(path);
  555. if (_sftpSession is null)
  556. {
  557. throw new SshConnectionException("Client not connected.");
  558. }
  559. cancellationToken.ThrowIfCancellationRequested();
  560. var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
  561. var handle = await _sftpSession.RequestOpenDirAsync(fullPath, cancellationToken).ConfigureAwait(false);
  562. try
  563. {
  564. var basePath = (fullPath[fullPath.Length - 1] == '/') ?
  565. fullPath :
  566. fullPath + '/';
  567. while (true)
  568. {
  569. var files = await _sftpSession.RequestReadDirAsync(handle, cancellationToken).ConfigureAwait(false);
  570. if (files is null)
  571. {
  572. break;
  573. }
  574. foreach (var file in files)
  575. {
  576. yield return new SftpFile(_sftpSession, basePath + file.Key, file.Value);
  577. }
  578. }
  579. }
  580. finally
  581. {
  582. await _sftpSession.RequestCloseAsync(handle, cancellationToken).ConfigureAwait(false);
  583. }
  584. }
  585. /// <summary>
  586. /// Begins an asynchronous operation of retrieving list of files in remote directory.
  587. /// </summary>
  588. /// <param name="path">The path.</param>
  589. /// <param name="asyncCallback">The method to be called when the asynchronous write operation is completed.</param>
  590. /// <param name="state">A user-provided object that distinguishes this particular asynchronous write request from other requests.</param>
  591. /// <param name="listCallback">The list callback.</param>
  592. /// <returns>
  593. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  594. /// </returns>
  595. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  596. public IAsyncResult BeginListDirectory(string path, AsyncCallback? asyncCallback, object? state, Action<int>? listCallback = null)
  597. {
  598. CheckDisposed();
  599. var asyncResult = new SftpListDirectoryAsyncResult(asyncCallback, state);
  600. ThreadAbstraction.ExecuteThread(() =>
  601. {
  602. try
  603. {
  604. var result = InternalListDirectory(path, asyncResult, listCallback);
  605. asyncResult.SetAsCompleted(result, completedSynchronously: false);
  606. }
  607. catch (Exception exp)
  608. {
  609. asyncResult.SetAsCompleted(exp, completedSynchronously: false);
  610. }
  611. });
  612. return asyncResult;
  613. }
  614. /// <summary>
  615. /// Ends an asynchronous operation of retrieving list of files in remote directory.
  616. /// </summary>
  617. /// <param name="asyncResult">The pending asynchronous SFTP request.</param>
  618. /// <returns>
  619. /// A list of files.
  620. /// </returns>
  621. /// <exception cref="ArgumentException">The <see cref="IAsyncResult"/> object did not come from the corresponding async method on this type.<para>-or-</para><see cref="EndListDirectory(IAsyncResult)"/> was called multiple times with the same <see cref="IAsyncResult"/>.</exception>
  622. public IEnumerable<ISftpFile> EndListDirectory(IAsyncResult asyncResult)
  623. {
  624. if (asyncResult is not SftpListDirectoryAsyncResult ar || ar.EndInvokeCalled)
  625. {
  626. throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult.");
  627. }
  628. // Wait for operation to complete, then return result or throw exception
  629. return ar.EndInvoke();
  630. }
  631. /// <summary>
  632. /// Gets reference to remote file or directory.
  633. /// </summary>
  634. /// <param name="path">The path.</param>
  635. /// <returns>
  636. /// A reference to <see cref="ISftpFile"/> file object.
  637. /// </returns>
  638. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  639. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  640. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  641. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  642. public ISftpFile Get(string path)
  643. {
  644. CheckDisposed();
  645. ThrowHelper.ThrowIfNull(path);
  646. if (_sftpSession is null)
  647. {
  648. throw new SshConnectionException("Client not connected.");
  649. }
  650. var fullPath = _sftpSession.GetCanonicalPath(path);
  651. var attributes = _sftpSession.RequestLStat(fullPath);
  652. return new SftpFile(_sftpSession, fullPath, attributes);
  653. }
  654. /// <summary>
  655. /// Gets reference to remote file or directory.
  656. /// </summary>
  657. /// <param name="path">The path.</param>
  658. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  659. /// <returns>
  660. /// A <see cref="Task{ISftpFile}"/> that represents the get operation.
  661. /// The task result contains the reference to <see cref="ISftpFile"/> file object.
  662. /// </returns>
  663. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  664. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  665. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  666. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  667. public async Task<ISftpFile> GetAsync(string path, CancellationToken cancellationToken)
  668. {
  669. CheckDisposed();
  670. ThrowHelper.ThrowIfNull(path);
  671. if (_sftpSession is null)
  672. {
  673. throw new SshConnectionException("Client not connected.");
  674. }
  675. cancellationToken.ThrowIfCancellationRequested();
  676. var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
  677. var attributes = await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
  678. return new SftpFile(_sftpSession, fullPath, attributes);
  679. }
  680. /// <summary>
  681. /// Checks whether file or directory exists.
  682. /// </summary>
  683. /// <param name="path">The path.</param>
  684. /// <returns>
  685. /// <see langword="true"/> if directory or file exists; otherwise <see langword="false"/>.
  686. /// </returns>
  687. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  688. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  689. /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  690. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  691. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  692. public bool Exists(string path)
  693. {
  694. CheckDisposed();
  695. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  696. if (_sftpSession is null)
  697. {
  698. throw new SshConnectionException("Client not connected.");
  699. }
  700. var fullPath = _sftpSession.GetCanonicalPath(path);
  701. /*
  702. * Using SSH_FXP_REALPATH is not an alternative as the SFTP specification has not always
  703. * been clear on how the server should respond when the specified path is not present on
  704. * the server:
  705. *
  706. * SSH 1 to 4:
  707. * No mention of how the server should respond if the path is not present on the server.
  708. *
  709. * SSH 5:
  710. * The server SHOULD fail the request if the path is not present on the server.
  711. *
  712. * SSH 6:
  713. * Draft 06: The server SHOULD fail the request if the path is not present on the server.
  714. * Draft 07 to 13: The server MUST NOT fail the request if the path does not exist.
  715. *
  716. * Note that SSH 6 (draft 06 and forward) allows for more control options, but we
  717. * currently only support up to v3.
  718. */
  719. try
  720. {
  721. _ = _sftpSession.RequestLStat(fullPath);
  722. return true;
  723. }
  724. catch (SftpPathNotFoundException)
  725. {
  726. return false;
  727. }
  728. }
  729. /// <summary>
  730. /// Checks whether file or directory exists.
  731. /// </summary>
  732. /// <param name="path">The path.</param>
  733. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  734. /// <returns>
  735. /// A <see cref="Task{T}"/> that represents the exists operation.
  736. /// The task result contains <see langword="true"/> if directory or file exists; otherwise <see langword="false"/>.
  737. /// </returns>
  738. /// <exception cref="ArgumentException"><paramref name="path"/> is <see langword="null"/> or contains only whitespace characters.</exception>
  739. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  740. /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  741. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message"/> is the message from the remote host.</exception>
  742. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  743. public async Task<bool> ExistsAsync(string path, CancellationToken cancellationToken = default)
  744. {
  745. CheckDisposed();
  746. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  747. if (_sftpSession is null)
  748. {
  749. throw new SshConnectionException("Client not connected.");
  750. }
  751. cancellationToken.ThrowIfCancellationRequested();
  752. var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
  753. /*
  754. * Using SSH_FXP_REALPATH is not an alternative as the SFTP specification has not always
  755. * been clear on how the server should respond when the specified path is not present on
  756. * the server:
  757. *
  758. * SSH 1 to 4:
  759. * No mention of how the server should respond if the path is not present on the server.
  760. *
  761. * SSH 5:
  762. * The server SHOULD fail the request if the path is not present on the server.
  763. *
  764. * SSH 6:
  765. * Draft 06: The server SHOULD fail the request if the path is not present on the server.
  766. * Draft 07 to 13: The server MUST NOT fail the request if the path does not exist.
  767. *
  768. * Note that SSH 6 (draft 06 and forward) allows for more control options, but we
  769. * currently only support up to v3.
  770. */
  771. try
  772. {
  773. _ = await _sftpSession.RequestLStatAsync(fullPath, cancellationToken).ConfigureAwait(false);
  774. return true;
  775. }
  776. catch (SftpPathNotFoundException)
  777. {
  778. return false;
  779. }
  780. }
  781. /// <summary>
  782. /// Downloads remote file specified by the path into the stream.
  783. /// </summary>
  784. /// <param name="path">File to download.</param>
  785. /// <param name="output">Stream to write the file into.</param>
  786. /// <param name="downloadCallback">The download callback.</param>
  787. /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
  788. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  789. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  790. /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  791. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>///
  792. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  793. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  794. /// <remarks>
  795. /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
  796. /// </remarks>
  797. public void DownloadFile(string path, Stream output, Action<ulong>? downloadCallback = null)
  798. {
  799. CheckDisposed();
  800. InternalDownloadFile(path, output, asyncResult: null, downloadCallback);
  801. }
  802. /// <summary>
  803. /// Begins an asynchronous file downloading into the stream.
  804. /// </summary>
  805. /// <param name="path">The path.</param>
  806. /// <param name="output">The output.</param>
  807. /// <returns>
  808. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  809. /// </returns>
  810. /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
  811. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  812. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  813. /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  814. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  815. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  816. /// <remarks>
  817. /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
  818. /// </remarks>
  819. public IAsyncResult BeginDownloadFile(string path, Stream output)
  820. {
  821. return BeginDownloadFile(path, output, asyncCallback: null, state: null);
  822. }
  823. /// <summary>
  824. /// Begins an asynchronous file downloading into the stream.
  825. /// </summary>
  826. /// <param name="path">The path.</param>
  827. /// <param name="output">The output.</param>
  828. /// <param name="asyncCallback">The method to be called when the asynchronous write operation is completed.</param>
  829. /// <returns>
  830. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  831. /// </returns>
  832. /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
  833. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  834. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  835. /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  836. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  837. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  838. /// <remarks>
  839. /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
  840. /// </remarks>
  841. public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback? asyncCallback)
  842. {
  843. return BeginDownloadFile(path, output, asyncCallback, state: null);
  844. }
  845. /// <summary>
  846. /// Begins an asynchronous file downloading into the stream.
  847. /// </summary>
  848. /// <param name="path">The path.</param>
  849. /// <param name="output">The output.</param>
  850. /// <param name="asyncCallback">The method to be called when the asynchronous write operation is completed.</param>
  851. /// <param name="state">A user-provided object that distinguishes this particular asynchronous write request from other requests.</param>
  852. /// <param name="downloadCallback">The download callback.</param>
  853. /// <returns>
  854. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  855. /// </returns>
  856. /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
  857. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  858. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  859. /// <remarks>
  860. /// Method calls made by this method to <paramref name="output" />, may under certain conditions result in exceptions thrown by the stream.
  861. /// </remarks>
  862. public IAsyncResult BeginDownloadFile(string path, Stream output, AsyncCallback? asyncCallback, object? state, Action<ulong>? downloadCallback = null)
  863. {
  864. CheckDisposed();
  865. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  866. ThrowHelper.ThrowIfNull(output);
  867. var asyncResult = new SftpDownloadAsyncResult(asyncCallback, state);
  868. ThreadAbstraction.ExecuteThread(() =>
  869. {
  870. try
  871. {
  872. InternalDownloadFile(path, output, asyncResult, downloadCallback);
  873. asyncResult.SetAsCompleted(exception: null, completedSynchronously: false);
  874. }
  875. catch (Exception exp)
  876. {
  877. asyncResult.SetAsCompleted(exp, completedSynchronously: false);
  878. }
  879. });
  880. return asyncResult;
  881. }
  882. /// <summary>
  883. /// Ends an asynchronous file downloading into the stream.
  884. /// </summary>
  885. /// <param name="asyncResult">The pending asynchronous SFTP request.</param>
  886. /// <exception cref="ArgumentException">The <see cref="IAsyncResult"/> object did not come from the corresponding async method on this type.<para>-or-</para><see cref="EndDownloadFile(IAsyncResult)"/> was called multiple times with the same <see cref="IAsyncResult"/>.</exception>
  887. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  888. /// <exception cref="SftpPermissionDeniedException">Permission to perform the operation was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  889. /// <exception cref="SftpPathNotFoundException">The path was not found on the remote host.</exception>
  890. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  891. public void EndDownloadFile(IAsyncResult asyncResult)
  892. {
  893. if (asyncResult is not SftpDownloadAsyncResult ar || ar.EndInvokeCalled)
  894. {
  895. throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult.");
  896. }
  897. // Wait for operation to complete, then return result or throw exception
  898. ar.EndInvoke();
  899. }
  900. /// <summary>
  901. /// Uploads stream into remote file.
  902. /// </summary>
  903. /// <param name="input">Data input stream.</param>
  904. /// <param name="path">Remote file path.</param>
  905. /// <param name="uploadCallback">The upload callback.</param>
  906. /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
  907. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  908. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  909. /// <exception cref="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  910. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  911. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  912. /// <remarks>
  913. /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
  914. /// </remarks>
  915. public void UploadFile(Stream input, string path, Action<ulong>? uploadCallback = null)
  916. {
  917. UploadFile(input, path, canOverride: true, uploadCallback);
  918. }
  919. /// <summary>
  920. /// Uploads stream into remote file.
  921. /// </summary>
  922. /// <param name="input">Data input stream.</param>
  923. /// <param name="path">Remote file path.</param>
  924. /// <param name="canOverride">if set to <see langword="true"/> then existing file will be overwritten.</param>
  925. /// <param name="uploadCallback">The upload callback.</param>
  926. /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
  927. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  928. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  929. /// <exception cref="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  930. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  931. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  932. /// <remarks>
  933. /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
  934. /// </remarks>
  935. public void UploadFile(Stream input, string path, bool canOverride, Action<ulong>? uploadCallback = null)
  936. {
  937. CheckDisposed();
  938. var flags = Flags.Write | Flags.Truncate;
  939. if (canOverride)
  940. {
  941. flags |= Flags.CreateNewOrOpen;
  942. }
  943. else
  944. {
  945. flags |= Flags.CreateNew;
  946. }
  947. InternalUploadFile(input, path, flags, asyncResult: null, uploadCallback);
  948. }
  949. /// <summary>
  950. /// Begins an asynchronous uploading the stream into remote file.
  951. /// </summary>
  952. /// <param name="input">Data input stream.</param>
  953. /// <param name="path">Remote file path.</param>
  954. /// <returns>
  955. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  956. /// </returns>
  957. /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
  958. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  959. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  960. /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  961. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  962. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  963. /// <remarks>
  964. /// <para>
  965. /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
  966. /// </para>
  967. /// <para>
  968. /// If the remote file already exists, it is overwritten and truncated.
  969. /// </para>
  970. /// </remarks>
  971. public IAsyncResult BeginUploadFile(Stream input, string path)
  972. {
  973. return BeginUploadFile(input, path, canOverride: true, asyncCallback: null, state: null);
  974. }
  975. /// <summary>
  976. /// Begins an asynchronous uploading the stream into remote file.
  977. /// </summary>
  978. /// <param name="input">Data input stream.</param>
  979. /// <param name="path">Remote file path.</param>
  980. /// <param name="asyncCallback">The method to be called when the asynchronous write operation is completed.</param>
  981. /// <returns>
  982. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  983. /// </returns>
  984. /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
  985. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  986. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  987. /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  988. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  989. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  990. /// <remarks>
  991. /// <para>
  992. /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
  993. /// </para>
  994. /// <para>
  995. /// If the remote file already exists, it is overwritten and truncated.
  996. /// </para>
  997. /// </remarks>
  998. public IAsyncResult BeginUploadFile(Stream input, string path, AsyncCallback? asyncCallback)
  999. {
  1000. return BeginUploadFile(input, path, canOverride: true, asyncCallback, state: null);
  1001. }
  1002. /// <summary>
  1003. /// Begins an asynchronous uploading the stream into remote file.
  1004. /// </summary>
  1005. /// <param name="input">Data input stream.</param>
  1006. /// <param name="path">Remote file path.</param>
  1007. /// <param name="asyncCallback">The method to be called when the asynchronous write operation is completed.</param>
  1008. /// <param name="state">A user-provided object that distinguishes this particular asynchronous write request from other requests.</param>
  1009. /// <param name="uploadCallback">The upload callback.</param>
  1010. /// <returns>
  1011. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  1012. /// </returns>
  1013. /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
  1014. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  1015. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1016. /// <exception cref="SftpPermissionDeniedException">Permission to list the contents of the directory was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  1017. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  1018. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1019. /// <remarks>
  1020. /// <para>
  1021. /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
  1022. /// </para>
  1023. /// <para>
  1024. /// If the remote file already exists, it is overwritten and truncated.
  1025. /// </para>
  1026. /// </remarks>
  1027. public IAsyncResult BeginUploadFile(Stream input, string path, AsyncCallback? asyncCallback, object? state, Action<ulong>? uploadCallback = null)
  1028. {
  1029. return BeginUploadFile(input, path, canOverride: true, asyncCallback, state, uploadCallback);
  1030. }
  1031. /// <summary>
  1032. /// Begins an asynchronous uploading the stream into remote file.
  1033. /// </summary>
  1034. /// <param name="input">Data input stream.</param>
  1035. /// <param name="path">Remote file path.</param>
  1036. /// <param name="canOverride">Specified whether an existing file can be overwritten.</param>
  1037. /// <param name="asyncCallback">The method to be called when the asynchronous write operation is completed.</param>
  1038. /// <param name="state">A user-provided object that distinguishes this particular asynchronous write request from other requests.</param>
  1039. /// <param name="uploadCallback">The upload callback.</param>
  1040. /// <returns>
  1041. /// An <see cref="IAsyncResult" /> that references the asynchronous operation.
  1042. /// </returns>
  1043. /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
  1044. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains only whitespace characters.</exception>
  1045. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1046. /// <remarks>
  1047. /// <para>
  1048. /// Method calls made by this method to <paramref name="input" />, may under certain conditions result in exceptions thrown by the stream.
  1049. /// </para>
  1050. /// <para>
  1051. /// When <paramref name="path"/> refers to an existing file, set <paramref name="canOverride"/> to <see langword="true"/> to overwrite and truncate that file.
  1052. /// If <paramref name="canOverride"/> is <see langword="false"/>, the upload will fail and <see cref="EndUploadFile(IAsyncResult)"/> will throw an
  1053. /// <see cref="SshException"/>.
  1054. /// </para>
  1055. /// </remarks>
  1056. public IAsyncResult BeginUploadFile(Stream input, string path, bool canOverride, AsyncCallback? asyncCallback, object? state, Action<ulong>? uploadCallback = null)
  1057. {
  1058. CheckDisposed();
  1059. ThrowHelper.ThrowIfNull(input);
  1060. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  1061. var flags = Flags.Write | Flags.Truncate;
  1062. if (canOverride)
  1063. {
  1064. flags |= Flags.CreateNewOrOpen;
  1065. }
  1066. else
  1067. {
  1068. flags |= Flags.CreateNew;
  1069. }
  1070. var asyncResult = new SftpUploadAsyncResult(asyncCallback, state);
  1071. ThreadAbstraction.ExecuteThread(() =>
  1072. {
  1073. try
  1074. {
  1075. InternalUploadFile(input, path, flags, asyncResult, uploadCallback);
  1076. asyncResult.SetAsCompleted(exception: null, completedSynchronously: false);
  1077. }
  1078. catch (Exception exp)
  1079. {
  1080. asyncResult.SetAsCompleted(exception: exp, completedSynchronously: false);
  1081. }
  1082. });
  1083. return asyncResult;
  1084. }
  1085. /// <summary>
  1086. /// Ends an asynchronous uploading the stream into remote file.
  1087. /// </summary>
  1088. /// <param name="asyncResult">The pending asynchronous SFTP request.</param>
  1089. /// <exception cref="ArgumentException">The <see cref="IAsyncResult"/> object did not come from the corresponding async method on this type.<para>-or-</para><see cref="EndUploadFile(IAsyncResult)"/> was called multiple times with the same <see cref="IAsyncResult"/>.</exception>
  1090. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1091. /// <exception cref="SftpPathNotFoundException">The directory of the file was not found on the remote host.</exception>
  1092. /// <exception cref="SftpPermissionDeniedException">Permission to upload the file was denied by the remote host. <para>-or-</para> A SSH command was denied by the server.</exception>
  1093. /// <exception cref="SshException">A SSH error where <see cref="Exception.Message" /> is the message from the remote host.</exception>
  1094. public void EndUploadFile(IAsyncResult asyncResult)
  1095. {
  1096. if (asyncResult is not SftpUploadAsyncResult ar || ar.EndInvokeCalled)
  1097. {
  1098. throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult.");
  1099. }
  1100. // Wait for operation to complete, then return result or throw exception
  1101. ar.EndInvoke();
  1102. }
  1103. /// <summary>
  1104. /// Gets status using statvfs@openssh.com request.
  1105. /// </summary>
  1106. /// <param name="path">The path.</param>
  1107. /// <returns>
  1108. /// A <see cref="SftpFileSystemInformation"/> instance that contains file status information.
  1109. /// </returns>
  1110. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1111. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  1112. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1113. public SftpFileSystemInformation GetStatus(string path)
  1114. {
  1115. CheckDisposed();
  1116. ThrowHelper.ThrowIfNull(path);
  1117. if (_sftpSession is null)
  1118. {
  1119. throw new SshConnectionException("Client not connected.");
  1120. }
  1121. var fullPath = _sftpSession.GetCanonicalPath(path);
  1122. return _sftpSession.RequestStatVfs(fullPath);
  1123. }
  1124. /// <summary>
  1125. /// Asynchronously gets status using statvfs@openssh.com request.
  1126. /// </summary>
  1127. /// <param name="path">The path.</param>
  1128. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  1129. /// <returns>
  1130. /// A <see cref="Task{SftpFileSystemInformation}"/> that represents the status operation.
  1131. /// The task result contains the <see cref="SftpFileSystemInformation"/> instance that contains file status information.
  1132. /// </returns>
  1133. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1134. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  1135. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1136. public async Task<SftpFileSystemInformation> GetStatusAsync(string path, CancellationToken cancellationToken)
  1137. {
  1138. CheckDisposed();
  1139. ThrowHelper.ThrowIfNull(path);
  1140. if (_sftpSession is null)
  1141. {
  1142. throw new SshConnectionException("Client not connected.");
  1143. }
  1144. cancellationToken.ThrowIfCancellationRequested();
  1145. var fullPath = await _sftpSession.GetCanonicalPathAsync(path, cancellationToken).ConfigureAwait(false);
  1146. return await _sftpSession.RequestStatVfsAsync(fullPath, cancellationToken).ConfigureAwait(false);
  1147. }
  1148. #region File Methods
  1149. /// <summary>
  1150. /// Appends lines to a file, creating the file if it does not already exist.
  1151. /// </summary>
  1152. /// <param name="path">The file to append the lines to. The file is created if it does not already exist.</param>
  1153. /// <param name="contents">The lines to append to the file.</param>
  1154. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>. <para>-or-</para> <paramref name="contents"/> is <see langword="null"/>.</exception>
  1155. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1156. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1157. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1158. /// <remarks>
  1159. /// The characters are written to the file using UTF-8 encoding without a byte-order mark (BOM).
  1160. /// </remarks>
  1161. public void AppendAllLines(string path, IEnumerable<string> contents)
  1162. {
  1163. CheckDisposed();
  1164. ThrowHelper.ThrowIfNull(contents);
  1165. using (var stream = AppendText(path))
  1166. {
  1167. foreach (var line in contents)
  1168. {
  1169. stream.WriteLine(line);
  1170. }
  1171. }
  1172. }
  1173. /// <summary>
  1174. /// Appends lines to a file by using a specified encoding, creating the file if it does not already exist.
  1175. /// </summary>
  1176. /// <param name="path">The file to append the lines to. The file is created if it does not already exist.</param>
  1177. /// <param name="contents">The lines to append to the file.</param>
  1178. /// <param name="encoding">The character encoding to use.</param>
  1179. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>. <para>-or-</para> <paramref name="contents"/> is <see langword="null"/>. <para>-or-</para> <paramref name="encoding"/> is <see langword="null"/>.</exception>
  1180. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1181. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1182. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1183. public void AppendAllLines(string path, IEnumerable<string> contents, Encoding encoding)
  1184. {
  1185. CheckDisposed();
  1186. ThrowHelper.ThrowIfNull(contents);
  1187. using (var stream = AppendText(path, encoding))
  1188. {
  1189. foreach (var line in contents)
  1190. {
  1191. stream.WriteLine(line);
  1192. }
  1193. }
  1194. }
  1195. /// <summary>
  1196. /// Appends the specified string to the file, creating the file if it does not already exist.
  1197. /// </summary>
  1198. /// <param name="path">The file to append the specified string to.</param>
  1199. /// <param name="contents">The string to append to the file.</param>
  1200. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>. <para>-or-</para> <paramref name="contents"/> is <see langword="null"/>.</exception>
  1201. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1202. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1203. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1204. /// <remarks>
  1205. /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM).
  1206. /// </remarks>
  1207. public void AppendAllText(string path, string contents)
  1208. {
  1209. using (var stream = AppendText(path))
  1210. {
  1211. stream.Write(contents);
  1212. }
  1213. }
  1214. /// <summary>
  1215. /// Appends the specified string to the file, creating the file if it does not already exist.
  1216. /// </summary>
  1217. /// <param name="path">The file to append the specified string to.</param>
  1218. /// <param name="contents">The string to append to the file.</param>
  1219. /// <param name="encoding">The character encoding to use.</param>
  1220. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>. <para>-or-</para> <paramref name="contents"/> is <see langword="null"/>. <para>-or-</para> <paramref name="encoding"/> is <see langword="null"/>.</exception>
  1221. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1222. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1223. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1224. public void AppendAllText(string path, string contents, Encoding encoding)
  1225. {
  1226. using (var stream = AppendText(path, encoding))
  1227. {
  1228. stream.Write(contents);
  1229. }
  1230. }
  1231. /// <summary>
  1232. /// Creates a <see cref="StreamWriter"/> that appends UTF-8 encoded text to the specified file,
  1233. /// creating the file if it does not already exist.
  1234. /// </summary>
  1235. /// <param name="path">The path to the file to append to.</param>
  1236. /// <returns>
  1237. /// A <see cref="StreamWriter"/> that appends text to a file using UTF-8 encoding without a
  1238. /// Byte-Order Mark (BOM).
  1239. /// </returns>
  1240. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1241. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1242. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1243. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1244. public StreamWriter AppendText(string path)
  1245. {
  1246. return AppendText(path, Utf8NoBOM);
  1247. }
  1248. /// <summary>
  1249. /// Creates a <see cref="StreamWriter"/> that appends text to a file using the specified
  1250. /// encoding, creating the file if it does not already exist.
  1251. /// </summary>
  1252. /// <param name="path">The path to the file to append to.</param>
  1253. /// <param name="encoding">The character encoding to use.</param>
  1254. /// <returns>
  1255. /// A <see cref="StreamWriter"/> that appends text to a file using the specified encoding.
  1256. /// </returns>
  1257. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>. <para>-or-</para> <paramref name="encoding"/> is <see langword="null"/>.</exception>
  1258. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1259. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1260. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1261. public StreamWriter AppendText(string path, Encoding encoding)
  1262. {
  1263. CheckDisposed();
  1264. ThrowHelper.ThrowIfNull(encoding);
  1265. return new StreamWriter(new SftpFileStream(_sftpSession, path, FileMode.Append, FileAccess.Write, (int)_bufferSize), encoding);
  1266. }
  1267. /// <summary>
  1268. /// Creates or overwrites a file in the specified path.
  1269. /// </summary>
  1270. /// <param name="path">The path and name of the file to create.</param>
  1271. /// <returns>
  1272. /// A <see cref="SftpFileStream"/> that provides read/write access to the file specified in path.
  1273. /// </returns>
  1274. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1275. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1276. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1277. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1278. /// <remarks>
  1279. /// If the target file already exists, it is first truncated to zero bytes.
  1280. /// </remarks>
  1281. public SftpFileStream Create(string path)
  1282. {
  1283. CheckDisposed();
  1284. return new SftpFileStream(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, (int)_bufferSize);
  1285. }
  1286. /// <summary>
  1287. /// Creates or overwrites the specified file.
  1288. /// </summary>
  1289. /// <param name="path">The path and name of the file to create.</param>
  1290. /// <param name="bufferSize">The maximum number of bytes buffered for reads and writes to the file.</param>
  1291. /// <returns>
  1292. /// A <see cref="SftpFileStream"/> that provides read/write access to the file specified in path.
  1293. /// </returns>
  1294. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1295. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1296. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1297. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1298. /// <remarks>
  1299. /// If the target file already exists, it is first truncated to zero bytes.
  1300. /// </remarks>
  1301. public SftpFileStream Create(string path, int bufferSize)
  1302. {
  1303. CheckDisposed();
  1304. return new SftpFileStream(_sftpSession, path, FileMode.Create, FileAccess.ReadWrite, bufferSize);
  1305. }
  1306. /// <summary>
  1307. /// Creates or opens a file for writing UTF-8 encoded text.
  1308. /// </summary>
  1309. /// <param name="path">The file to be opened for writing.</param>
  1310. /// <returns>
  1311. /// A <see cref="StreamWriter"/> that writes text to a file using UTF-8 encoding without
  1312. /// a Byte-Order Mark (BOM).
  1313. /// </returns>
  1314. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1315. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1316. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1317. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1318. /// <remarks>
  1319. /// <para>
  1320. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1321. /// </para>
  1322. /// <para>
  1323. /// If the target file does not exist, it is created.
  1324. /// </para>
  1325. /// </remarks>
  1326. public StreamWriter CreateText(string path)
  1327. {
  1328. return CreateText(path, Utf8NoBOM);
  1329. }
  1330. /// <summary>
  1331. /// Creates or opens a file for writing text using the specified encoding.
  1332. /// </summary>
  1333. /// <param name="path">The file to be opened for writing.</param>
  1334. /// <param name="encoding">The character encoding to use.</param>
  1335. /// <returns>
  1336. /// A <see cref="StreamWriter"/> that writes to a file using the specified encoding.
  1337. /// </returns>
  1338. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1339. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1340. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1341. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1342. /// <remarks>
  1343. /// <para>
  1344. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1345. /// </para>
  1346. /// <para>
  1347. /// If the target file does not exist, it is created.
  1348. /// </para>
  1349. /// </remarks>
  1350. public StreamWriter CreateText(string path, Encoding encoding)
  1351. {
  1352. CheckDisposed();
  1353. return new StreamWriter(OpenWrite(path), encoding);
  1354. }
  1355. /// <summary>
  1356. /// Deletes the specified file or directory.
  1357. /// </summary>
  1358. /// <param name="path">The name of the file or directory to be deleted. Wildcard characters are not supported.</param>
  1359. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1360. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1361. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  1362. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1363. public void Delete(string path)
  1364. {
  1365. var file = Get(path);
  1366. file.Delete();
  1367. }
  1368. /// <summary>
  1369. /// Returns the date and time the specified file or directory was last accessed.
  1370. /// </summary>
  1371. /// <param name="path">The file or directory for which to obtain access date and time information.</param>
  1372. /// <returns>
  1373. /// A <see cref="DateTime"/> structure set to the date and time that the specified file or directory was last accessed.
  1374. /// This value is expressed in local time.
  1375. /// </returns>
  1376. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1377. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1378. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1379. public DateTime GetLastAccessTime(string path)
  1380. {
  1381. var file = Get(path);
  1382. return file.LastAccessTime;
  1383. }
  1384. /// <summary>
  1385. /// Returns the date and time, in coordinated universal time (UTC), that the specified file or directory was last accessed.
  1386. /// </summary>
  1387. /// <param name="path">The file or directory for which to obtain access date and time information.</param>
  1388. /// <returns>
  1389. /// A <see cref="DateTime"/> structure set to the date and time that the specified file or directory was last accessed.
  1390. /// This value is expressed in UTC time.
  1391. /// </returns>
  1392. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1393. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1394. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1395. public DateTime GetLastAccessTimeUtc(string path)
  1396. {
  1397. var lastAccessTime = GetLastAccessTime(path);
  1398. return lastAccessTime.ToUniversalTime();
  1399. }
  1400. /// <summary>
  1401. /// Returns the date and time the specified file or directory was last written to.
  1402. /// </summary>
  1403. /// <param name="path">The file or directory for which to obtain write date and time information.</param>
  1404. /// <returns>
  1405. /// A <see cref="DateTime"/> structure set to the date and time that the specified file or directory was last written to.
  1406. /// This value is expressed in local time.
  1407. /// </returns>
  1408. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1409. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1410. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1411. public DateTime GetLastWriteTime(string path)
  1412. {
  1413. var file = Get(path);
  1414. return file.LastWriteTime;
  1415. }
  1416. /// <summary>
  1417. /// Returns the date and time, in coordinated universal time (UTC), that the specified file or directory was last written to.
  1418. /// </summary>
  1419. /// <param name="path">The file or directory for which to obtain write date and time information.</param>
  1420. /// <returns>
  1421. /// A <see cref="DateTime"/> structure set to the date and time that the specified file or directory was last written to.
  1422. /// This value is expressed in UTC time.
  1423. /// </returns>
  1424. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1425. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1426. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1427. public DateTime GetLastWriteTimeUtc(string path)
  1428. {
  1429. var lastWriteTime = GetLastWriteTime(path);
  1430. return lastWriteTime.ToUniversalTime();
  1431. }
  1432. /// <summary>
  1433. /// Opens a <see cref="SftpFileStream"/> on the specified path with read/write access.
  1434. /// </summary>
  1435. /// <param name="path">The file to open.</param>
  1436. /// <param name="mode">A <see cref="FileMode"/> 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.</param>
  1437. /// <returns>
  1438. /// An unshared <see cref="SftpFileStream"/> that provides access to the specified file, with the specified mode and read/write access.
  1439. /// </returns>
  1440. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1441. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1442. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1443. public SftpFileStream Open(string path, FileMode mode)
  1444. {
  1445. return Open(path, mode, FileAccess.ReadWrite);
  1446. }
  1447. /// <summary>
  1448. /// Opens a <see cref="SftpFileStream"/> on the specified path, with the specified mode and access.
  1449. /// </summary>
  1450. /// <param name="path">The file to open.</param>
  1451. /// <param name="mode">A <see cref="FileMode"/> 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.</param>
  1452. /// <param name="access">A <see cref="FileAccess"/> value that specifies the operations that can be performed on the file.</param>
  1453. /// <returns>
  1454. /// An unshared <see cref="SftpFileStream"/> that provides access to the specified file, with the specified mode and access.
  1455. /// </returns>
  1456. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1457. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1458. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1459. public SftpFileStream Open(string path, FileMode mode, FileAccess access)
  1460. {
  1461. CheckDisposed();
  1462. return new SftpFileStream(_sftpSession, path, mode, access, (int)_bufferSize);
  1463. }
  1464. /// <summary>
  1465. /// Asynchronously opens a <see cref="SftpFileStream"/> on the specified path, with the specified mode and access.
  1466. /// </summary>
  1467. /// <param name="path">The file to open.</param>
  1468. /// <param name="mode">A <see cref="FileMode"/> 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.</param>
  1469. /// <param name="access">A <see cref="FileAccess"/> value that specifies the operations that can be performed on the file.</param>
  1470. /// <param name="cancellationToken">The <see cref="CancellationToken"/> to observe.</param>
  1471. /// <returns>
  1472. /// A <see cref="Task{SftpFileStream}"/> that represents the asynchronous open operation.
  1473. /// The task result contains the <see cref="SftpFileStream"/> that provides access to the specified file, with the specified mode and access.
  1474. /// </returns>
  1475. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1476. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1477. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1478. public Task<SftpFileStream> OpenAsync(string path, FileMode mode, FileAccess access, CancellationToken cancellationToken)
  1479. {
  1480. CheckDisposed();
  1481. ThrowHelper.ThrowIfNull(path);
  1482. if (_sftpSession is null)
  1483. {
  1484. throw new SshConnectionException("Client not connected.");
  1485. }
  1486. cancellationToken.ThrowIfCancellationRequested();
  1487. return SftpFileStream.OpenAsync(_sftpSession, path, mode, access, (int)_bufferSize, cancellationToken);
  1488. }
  1489. /// <summary>
  1490. /// Opens an existing file for reading.
  1491. /// </summary>
  1492. /// <param name="path">The file to be opened for reading.</param>
  1493. /// <returns>
  1494. /// A read-only <see cref="SftpFileStream"/> on the specified path.
  1495. /// </returns>
  1496. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1497. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1498. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1499. public SftpFileStream OpenRead(string path)
  1500. {
  1501. return Open(path, FileMode.Open, FileAccess.Read);
  1502. }
  1503. /// <summary>
  1504. /// Opens an existing UTF-8 encoded text file for reading.
  1505. /// </summary>
  1506. /// <param name="path">The file to be opened for reading.</param>
  1507. /// <returns>
  1508. /// A <see cref="StreamReader"/> on the specified path.
  1509. /// </returns>
  1510. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1511. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1512. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1513. public StreamReader OpenText(string path)
  1514. {
  1515. return new StreamReader(OpenRead(path), Encoding.UTF8);
  1516. }
  1517. /// <summary>
  1518. /// Opens a file for writing.
  1519. /// </summary>
  1520. /// <param name="path">The file to be opened for writing.</param>
  1521. /// <returns>
  1522. /// An unshared <see cref="SftpFileStream"/> object on the specified path with <see cref="FileAccess.Write"/> access.
  1523. /// </returns>
  1524. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1525. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1526. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1527. /// <remarks>
  1528. /// If the file does not exist, it is created.
  1529. /// </remarks>
  1530. public SftpFileStream OpenWrite(string path)
  1531. {
  1532. CheckDisposed();
  1533. return new SftpFileStream(_sftpSession, path, FileMode.OpenOrCreate, FileAccess.Write, (int)_bufferSize);
  1534. }
  1535. /// <summary>
  1536. /// Opens a binary file, reads the contents of the file into a byte array, and closes the file.
  1537. /// </summary>
  1538. /// <param name="path">The file to open for reading.</param>
  1539. /// <returns>
  1540. /// A byte array containing the contents of the file.
  1541. /// </returns>
  1542. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1543. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1544. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1545. public byte[] ReadAllBytes(string path)
  1546. {
  1547. using (var stream = OpenRead(path))
  1548. {
  1549. var buffer = new byte[stream.Length];
  1550. _ = stream.Read(buffer, 0, buffer.Length);
  1551. return buffer;
  1552. }
  1553. }
  1554. /// <summary>
  1555. /// Opens a text file, reads all lines of the file using UTF-8 encoding, and closes the file.
  1556. /// </summary>
  1557. /// <param name="path">The file to open for reading.</param>
  1558. /// <returns>
  1559. /// A string array containing all lines of the file.
  1560. /// </returns>
  1561. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1562. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1563. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1564. public string[] ReadAllLines(string path)
  1565. {
  1566. return ReadAllLines(path, Encoding.UTF8);
  1567. }
  1568. /// <summary>
  1569. /// Opens a file, reads all lines of the file with the specified encoding, and closes the file.
  1570. /// </summary>
  1571. /// <param name="path">The file to open for reading.</param>
  1572. /// <param name="encoding">The encoding applied to the contents of the file.</param>
  1573. /// <returns>
  1574. /// A string array containing all lines of the file.
  1575. /// </returns>
  1576. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1577. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1578. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1579. public string[] ReadAllLines(string path, Encoding encoding)
  1580. {
  1581. /*
  1582. * We use the default buffer size for StreamReader - which is 1024 bytes - and the configured buffer size
  1583. * for the SftpFileStream. We may want to revisit this later.
  1584. */
  1585. var lines = new List<string>();
  1586. using (var stream = new StreamReader(OpenRead(path), encoding))
  1587. {
  1588. string? line;
  1589. while ((line = stream.ReadLine()) != null)
  1590. {
  1591. lines.Add(line);
  1592. }
  1593. }
  1594. return lines.ToArray();
  1595. }
  1596. /// <summary>
  1597. /// Opens a text file, reads all lines of the file with the UTF-8 encoding, and closes the file.
  1598. /// </summary>
  1599. /// <param name="path">The file to open for reading.</param>
  1600. /// <returns>
  1601. /// A string containing all lines of the file.
  1602. /// </returns>
  1603. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1604. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1605. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1606. public string ReadAllText(string path)
  1607. {
  1608. return ReadAllText(path, Encoding.UTF8);
  1609. }
  1610. /// <summary>
  1611. /// Opens a file, reads all lines of the file with the specified encoding, and closes the file.
  1612. /// </summary>
  1613. /// <param name="path">The file to open for reading.</param>
  1614. /// <param name="encoding">The encoding applied to the contents of the file.</param>
  1615. /// <returns>
  1616. /// A string containing all lines of the file.
  1617. /// </returns>
  1618. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1619. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1620. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1621. public string ReadAllText(string path, Encoding encoding)
  1622. {
  1623. /*
  1624. * We use the default buffer size for StreamReader - which is 1024 bytes - and the configured buffer size
  1625. * for the SftpFileStream. We may want to revisit this later.
  1626. */
  1627. using (var stream = new StreamReader(OpenRead(path), encoding))
  1628. {
  1629. return stream.ReadToEnd();
  1630. }
  1631. }
  1632. /// <summary>
  1633. /// Reads the lines of a file with the UTF-8 encoding.
  1634. /// </summary>
  1635. /// <param name="path">The file to read.</param>
  1636. /// <returns>
  1637. /// The lines of the file.
  1638. /// </returns>
  1639. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1640. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1641. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1642. public IEnumerable<string> ReadLines(string path)
  1643. {
  1644. return ReadAllLines(path);
  1645. }
  1646. /// <summary>
  1647. /// Read the lines of a file that has a specified encoding.
  1648. /// </summary>
  1649. /// <param name="path">The file to read.</param>
  1650. /// <param name="encoding">The encoding that is applied to the contents of the file.</param>
  1651. /// <returns>
  1652. /// The lines of the file.
  1653. /// </returns>
  1654. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1655. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1656. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1657. public IEnumerable<string> ReadLines(string path, Encoding encoding)
  1658. {
  1659. return ReadAllLines(path, encoding);
  1660. }
  1661. /// <summary>
  1662. /// Sets the date and time the specified file was last accessed.
  1663. /// </summary>
  1664. /// <param name="path">The file for which to set the access date and time information.</param>
  1665. /// <param name="lastAccessTime">A <see cref="DateTime"/> containing the value to set for the last access date and time of path. This value is expressed in local time.</param>
  1666. public void SetLastAccessTime(string path, DateTime lastAccessTime)
  1667. {
  1668. var attributes = GetAttributes(path);
  1669. attributes.LastAccessTime = lastAccessTime;
  1670. SetAttributes(path, attributes);
  1671. }
  1672. /// <summary>
  1673. /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last accessed.
  1674. /// </summary>
  1675. /// <param name="path">The file for which to set the access date and time information.</param>
  1676. /// <param name="lastAccessTimeUtc">A <see cref="DateTime"/> containing the value to set for the last access date and time of path. This value is expressed in UTC time.</param>
  1677. public void SetLastAccessTimeUtc(string path, DateTime lastAccessTimeUtc)
  1678. {
  1679. var attributes = GetAttributes(path);
  1680. attributes.LastAccessTimeUtc = lastAccessTimeUtc;
  1681. SetAttributes(path, attributes);
  1682. }
  1683. /// <summary>
  1684. /// Sets the date and time that the specified file was last written to.
  1685. /// </summary>
  1686. /// <param name="path">The file for which to set the date and time information.</param>
  1687. /// <param name="lastWriteTime">A <see cref="DateTime"/> containing the value to set for the last write date and time of path. This value is expressed in local time.</param>
  1688. public void SetLastWriteTime(string path, DateTime lastWriteTime)
  1689. {
  1690. var attributes = GetAttributes(path);
  1691. attributes.LastWriteTime = lastWriteTime;
  1692. SetAttributes(path, attributes);
  1693. }
  1694. /// <summary>
  1695. /// Sets the date and time, in coordinated universal time (UTC), that the specified file was last written to.
  1696. /// </summary>
  1697. /// <param name="path">The file for which to set the date and time information.</param>
  1698. /// <param name="lastWriteTimeUtc">A <see cref="DateTime"/> containing the value to set for the last write date and time of path. This value is expressed in UTC time.</param>
  1699. public void SetLastWriteTimeUtc(string path, DateTime lastWriteTimeUtc)
  1700. {
  1701. var attributes = GetAttributes(path);
  1702. attributes.LastWriteTimeUtc = lastWriteTimeUtc;
  1703. SetAttributes(path, attributes);
  1704. }
  1705. /// <summary>
  1706. /// Writes the specified byte array to the specified file, and closes the file.
  1707. /// </summary>
  1708. /// <param name="path">The file to write to.</param>
  1709. /// <param name="bytes">The bytes to write to the file.</param>
  1710. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1711. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1712. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1713. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1714. /// <remarks>
  1715. /// <para>
  1716. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1717. /// </para>
  1718. /// <para>
  1719. /// If the target file does not exist, it is created.
  1720. /// </para>
  1721. /// </remarks>
  1722. public void WriteAllBytes(string path, byte[] bytes)
  1723. {
  1724. using (var stream = OpenWrite(path))
  1725. {
  1726. stream.Write(bytes, 0, bytes.Length);
  1727. }
  1728. }
  1729. /// <summary>
  1730. /// Writes a collection of strings to the file using the UTF-8 encoding, and closes the file.
  1731. /// </summary>
  1732. /// <param name="path">The file to write to.</param>
  1733. /// <param name="contents">The lines to write to the file.</param>
  1734. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1735. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1736. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1737. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1738. /// <remarks>
  1739. /// <para>
  1740. /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM).
  1741. /// </para>
  1742. /// <para>
  1743. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1744. /// </para>
  1745. /// <para>
  1746. /// If the target file does not exist, it is created.
  1747. /// </para>
  1748. /// </remarks>
  1749. public void WriteAllLines(string path, IEnumerable<string> contents)
  1750. {
  1751. WriteAllLines(path, contents, Utf8NoBOM);
  1752. }
  1753. /// <summary>
  1754. /// Write the specified string array to the file using the UTF-8 encoding, and closes the file.
  1755. /// </summary>
  1756. /// <param name="path">The file to write to.</param>
  1757. /// <param name="contents">The string array to write to the file.</param>
  1758. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1759. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1760. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1761. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1762. /// <remarks>
  1763. /// <para>
  1764. /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM).
  1765. /// </para>
  1766. /// <para>
  1767. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1768. /// </para>
  1769. /// <para>
  1770. /// If the target file does not exist, it is created.
  1771. /// </para>
  1772. /// </remarks>
  1773. public void WriteAllLines(string path, string[] contents)
  1774. {
  1775. WriteAllLines(path, contents, Utf8NoBOM);
  1776. }
  1777. /// <summary>
  1778. /// Writes a collection of strings to the file using the specified encoding, and closes the file.
  1779. /// </summary>
  1780. /// <param name="path">The file to write to.</param>
  1781. /// <param name="contents">The lines to write to the file.</param>
  1782. /// <param name="encoding">The character encoding to use.</param>
  1783. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1784. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1785. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1786. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1787. /// <remarks>
  1788. /// <para>
  1789. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1790. /// </para>
  1791. /// <para>
  1792. /// If the target file does not exist, it is created.
  1793. /// </para>
  1794. /// </remarks>
  1795. public void WriteAllLines(string path, IEnumerable<string> contents, Encoding encoding)
  1796. {
  1797. using (var stream = CreateText(path, encoding))
  1798. {
  1799. foreach (var line in contents)
  1800. {
  1801. stream.WriteLine(line);
  1802. }
  1803. }
  1804. }
  1805. /// <summary>
  1806. /// Writes the specified string array to the file by using the specified encoding, and closes the file.
  1807. /// </summary>
  1808. /// <param name="path">The file to write to.</param>
  1809. /// <param name="contents">The string array to write to the file.</param>
  1810. /// <param name="encoding">An <see cref="Encoding"/> object that represents the character encoding applied to the string array.</param>
  1811. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1812. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1813. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1814. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1815. /// <remarks>
  1816. /// <para>
  1817. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1818. /// </para>
  1819. /// <para>
  1820. /// If the target file does not exist, it is created.
  1821. /// </para>
  1822. /// </remarks>
  1823. public void WriteAllLines(string path, string[] contents, Encoding encoding)
  1824. {
  1825. using (var stream = CreateText(path, encoding))
  1826. {
  1827. foreach (var line in contents)
  1828. {
  1829. stream.WriteLine(line);
  1830. }
  1831. }
  1832. }
  1833. /// <summary>
  1834. /// Writes the specified string to the file using the UTF-8 encoding, and closes the file.
  1835. /// </summary>
  1836. /// <param name="path">The file to write to.</param>
  1837. /// <param name="contents">The string to write to the file.</param>
  1838. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1839. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1840. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1841. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1842. /// <remarks>
  1843. /// <para>
  1844. /// The characters are written to the file using UTF-8 encoding without a Byte-Order Mark (BOM).
  1845. /// </para>
  1846. /// <para>
  1847. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1848. /// </para>
  1849. /// <para>
  1850. /// If the target file does not exist, it is created.
  1851. /// </para>
  1852. /// </remarks>
  1853. public void WriteAllText(string path, string contents)
  1854. {
  1855. using (var stream = CreateText(path))
  1856. {
  1857. stream.Write(contents);
  1858. }
  1859. }
  1860. /// <summary>
  1861. /// Writes the specified string to the file using the specified encoding, and closes the file.
  1862. /// </summary>
  1863. /// <param name="path">The file to write to.</param>
  1864. /// <param name="contents">The string to write to the file.</param>
  1865. /// <param name="encoding">The encoding to apply to the string.</param>
  1866. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1867. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1868. /// <exception cref="SftpPathNotFoundException">The specified path is invalid, or its directory was not found on the remote host.</exception>
  1869. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1870. /// <remarks>
  1871. /// <para>
  1872. /// If the target file already exists, it is overwritten. It is not first truncated to zero bytes.
  1873. /// </para>
  1874. /// <para>
  1875. /// If the target file does not exist, it is created.
  1876. /// </para>
  1877. /// </remarks>
  1878. public void WriteAllText(string path, string contents, Encoding encoding)
  1879. {
  1880. using (var stream = CreateText(path, encoding))
  1881. {
  1882. stream.Write(contents);
  1883. }
  1884. }
  1885. /// <summary>
  1886. /// Gets the <see cref="SftpFileAttributes"/> of the file on the path.
  1887. /// </summary>
  1888. /// <param name="path">The path to the file.</param>
  1889. /// <returns>
  1890. /// The <see cref="SftpFileAttributes"/> of the file on the path.
  1891. /// </returns>
  1892. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1893. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1894. /// <exception cref="SftpPathNotFoundException"><paramref name="path"/> was not found on the remote host.</exception>
  1895. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1896. public SftpFileAttributes GetAttributes(string path)
  1897. {
  1898. CheckDisposed();
  1899. if (_sftpSession is null)
  1900. {
  1901. throw new SshConnectionException("Client not connected.");
  1902. }
  1903. var fullPath = _sftpSession.GetCanonicalPath(path);
  1904. return _sftpSession.RequestLStat(fullPath);
  1905. }
  1906. /// <summary>
  1907. /// Sets the specified <see cref="SftpFileAttributes"/> of the file on the specified path.
  1908. /// </summary>
  1909. /// <param name="path">The path to the file.</param>
  1910. /// <param name="fileAttributes">The desired <see cref="SftpFileAttributes"/>.</param>
  1911. /// <exception cref="ArgumentNullException"><paramref name="path"/> is <see langword="null"/>.</exception>
  1912. /// <exception cref="SshConnectionException">Client is not connected.</exception>
  1913. /// <exception cref="ObjectDisposedException">The method was called after the client was disposed.</exception>
  1914. public void SetAttributes(string path, SftpFileAttributes fileAttributes)
  1915. {
  1916. CheckDisposed();
  1917. if (_sftpSession is null)
  1918. {
  1919. throw new SshConnectionException("Client not connected.");
  1920. }
  1921. var fullPath = _sftpSession.GetCanonicalPath(path);
  1922. _sftpSession.RequestSetStat(fullPath, fileAttributes);
  1923. }
  1924. #endregion // File Methods
  1925. #region SynchronizeDirectories
  1926. /// <summary>
  1927. /// Synchronizes the directories.
  1928. /// </summary>
  1929. /// <param name="sourcePath">The source path.</param>
  1930. /// <param name="destinationPath">The destination path.</param>
  1931. /// <param name="searchPattern">The search pattern.</param>
  1932. /// <returns>
  1933. /// A list of uploaded files.
  1934. /// </returns>
  1935. /// <exception cref="ArgumentNullException"><paramref name="sourcePath"/> is <see langword="null"/>.</exception>
  1936. /// <exception cref="ArgumentException"><paramref name="destinationPath"/> is <see langword="null"/> or contains only whitespace.</exception>
  1937. /// <exception cref="SftpPathNotFoundException"><paramref name="destinationPath"/> was not found on the remote host.</exception>
  1938. /// <exception cref="SshException">If a problem occurs while copying the file.</exception>
  1939. public IEnumerable<FileInfo> SynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern)
  1940. {
  1941. ThrowHelper.ThrowIfNull(sourcePath);
  1942. ThrowHelper.ThrowIfNullOrWhiteSpace(destinationPath);
  1943. return InternalSynchronizeDirectories(sourcePath, destinationPath, searchPattern, asynchResult: null);
  1944. }
  1945. /// <summary>
  1946. /// Begins the synchronize directories.
  1947. /// </summary>
  1948. /// <param name="sourcePath">The source path.</param>
  1949. /// <param name="destinationPath">The destination path.</param>
  1950. /// <param name="searchPattern">The search pattern.</param>
  1951. /// <param name="asyncCallback">The async callback.</param>
  1952. /// <param name="state">The state.</param>
  1953. /// <returns>
  1954. /// An <see cref="IAsyncResult" /> that represents the asynchronous directory synchronization.
  1955. /// </returns>
  1956. /// <exception cref="ArgumentNullException"><paramref name="sourcePath"/> or <paramref name="searchPattern"/> is <see langword="null"/>.</exception>
  1957. /// <exception cref="ArgumentException"><paramref name="destinationPath"/> is <see langword="null"/> or contains only whitespace.</exception>
  1958. /// <exception cref="SshException">If a problem occurs while copying the file.</exception>
  1959. public IAsyncResult BeginSynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern, AsyncCallback? asyncCallback, object? state)
  1960. {
  1961. ThrowHelper.ThrowIfNull(sourcePath);
  1962. ThrowHelper.ThrowIfNullOrWhiteSpace(destinationPath);
  1963. ThrowHelper.ThrowIfNull(searchPattern);
  1964. var asyncResult = new SftpSynchronizeDirectoriesAsyncResult(asyncCallback, state);
  1965. ThreadAbstraction.ExecuteThread(() =>
  1966. {
  1967. try
  1968. {
  1969. var result = InternalSynchronizeDirectories(sourcePath, destinationPath, searchPattern, asyncResult);
  1970. asyncResult.SetAsCompleted(result, completedSynchronously: false);
  1971. }
  1972. catch (Exception exp)
  1973. {
  1974. asyncResult.SetAsCompleted(exp, completedSynchronously: false);
  1975. }
  1976. });
  1977. return asyncResult;
  1978. }
  1979. /// <summary>
  1980. /// Ends the synchronize directories.
  1981. /// </summary>
  1982. /// <param name="asyncResult">The async result.</param>
  1983. /// <returns>
  1984. /// A list of uploaded files.
  1985. /// </returns>
  1986. /// <exception cref="ArgumentException">The <see cref="IAsyncResult"/> object did not come from the corresponding async method on this type.<para>-or-</para><see cref="EndSynchronizeDirectories(IAsyncResult)"/> was called multiple times with the same <see cref="IAsyncResult"/>.</exception>
  1987. /// <exception cref="SftpPathNotFoundException">The destination path was not found on the remote host.</exception>
  1988. public IEnumerable<FileInfo> EndSynchronizeDirectories(IAsyncResult asyncResult)
  1989. {
  1990. if (asyncResult is not SftpSynchronizeDirectoriesAsyncResult ar || ar.EndInvokeCalled)
  1991. {
  1992. throw new ArgumentException("Either the IAsyncResult object did not come from the corresponding async method on this type, or EndExecute was called multiple times with the same IAsyncResult.");
  1993. }
  1994. // Wait for operation to complete, then return result or throw exception
  1995. return ar.EndInvoke();
  1996. }
  1997. private List<FileInfo> InternalSynchronizeDirectories(string sourcePath, string destinationPath, string searchPattern, SftpSynchronizeDirectoriesAsyncResult? asynchResult)
  1998. {
  1999. if (!Directory.Exists(sourcePath))
  2000. {
  2001. throw new FileNotFoundException(string.Format("Source directory not found: {0}", sourcePath));
  2002. }
  2003. var uploadedFiles = new List<FileInfo>();
  2004. var sourceDirectory = new DirectoryInfo(sourcePath);
  2005. using (var sourceFiles = sourceDirectory.EnumerateFiles(searchPattern).GetEnumerator())
  2006. {
  2007. if (!sourceFiles.MoveNext())
  2008. {
  2009. return uploadedFiles;
  2010. }
  2011. #region Existing Files at The Destination
  2012. var destFiles = InternalListDirectory(destinationPath, asyncResult: null, listCallback: null);
  2013. var destDict = new Dictionary<string, ISftpFile>();
  2014. foreach (var destFile in destFiles)
  2015. {
  2016. if (destFile.IsDirectory)
  2017. {
  2018. continue;
  2019. }
  2020. destDict.Add(destFile.Name, destFile);
  2021. }
  2022. #endregion
  2023. #region Upload the difference
  2024. const Flags uploadFlag = Flags.Write | Flags.Truncate | Flags.CreateNewOrOpen;
  2025. do
  2026. {
  2027. var localFile = sourceFiles.Current;
  2028. if (localFile is null)
  2029. {
  2030. continue;
  2031. }
  2032. var isDifferent = true;
  2033. if (destDict.TryGetValue(localFile.Name, out var remoteFile))
  2034. {
  2035. // File exists at the destination, use filesize to detect if there's a difference
  2036. isDifferent = localFile.Length != remoteFile.Length;
  2037. }
  2038. if (isDifferent)
  2039. {
  2040. var remoteFileName = string.Format(CultureInfo.InvariantCulture, @"{0}/{1}", destinationPath, localFile.Name);
  2041. try
  2042. {
  2043. #pragma warning disable CA2000 // Dispose objects before losing scope; false positive
  2044. using (var file = File.OpenRead(localFile.FullName))
  2045. #pragma warning restore CA2000 // Dispose objects before losing scope; false positive
  2046. {
  2047. InternalUploadFile(file, remoteFileName, uploadFlag, asyncResult: null, uploadCallback: null);
  2048. }
  2049. uploadedFiles.Add(localFile);
  2050. asynchResult?.Update(uploadedFiles.Count);
  2051. }
  2052. catch (Exception ex)
  2053. {
  2054. throw new SshException($"Failed to upload {localFile.FullName} to {remoteFileName}", ex);
  2055. }
  2056. }
  2057. }
  2058. while (sourceFiles.MoveNext());
  2059. }
  2060. #endregion
  2061. return uploadedFiles;
  2062. }
  2063. #endregion
  2064. /// <summary>
  2065. /// Internals the list directory.
  2066. /// </summary>
  2067. /// <param name="path">The path.</param>
  2068. /// <param name="asyncResult">An <see cref="IAsyncResult"/> that references the asynchronous request.</param>
  2069. /// <param name="listCallback">The list callback.</param>
  2070. /// <returns>
  2071. /// A list of files in the specfied directory.
  2072. /// </returns>
  2073. /// <exception cref="ArgumentNullException"><paramref name="path" /> is <see langword="null"/>.</exception>
  2074. /// <exception cref="SshConnectionException">Client not connected.</exception>
  2075. private List<ISftpFile> InternalListDirectory(string path, SftpListDirectoryAsyncResult? asyncResult, Action<int>? listCallback)
  2076. {
  2077. ThrowHelper.ThrowIfNull(path);
  2078. if (_sftpSession is null)
  2079. {
  2080. throw new SshConnectionException("Client not connected.");
  2081. }
  2082. var fullPath = _sftpSession.GetCanonicalPath(path);
  2083. var handle = _sftpSession.RequestOpenDir(fullPath);
  2084. var basePath = fullPath;
  2085. #if NET || NETSTANDARD2_1_OR_GREATER
  2086. if (!basePath.EndsWith('/'))
  2087. #else
  2088. if (!basePath.EndsWith("/", StringComparison.Ordinal))
  2089. #endif // NET || NETSTANDARD2_1_OR_GREATER
  2090. {
  2091. basePath = string.Format("{0}/", fullPath);
  2092. }
  2093. var result = new List<ISftpFile>();
  2094. var files = _sftpSession.RequestReadDir(handle);
  2095. while (files is not null)
  2096. {
  2097. foreach (var f in files)
  2098. {
  2099. result.Add(new SftpFile(_sftpSession,
  2100. string.Format(CultureInfo.InvariantCulture, "{0}{1}", basePath, f.Key),
  2101. f.Value));
  2102. }
  2103. asyncResult?.Update(result.Count);
  2104. // Call callback to report number of files read
  2105. if (listCallback is not null)
  2106. {
  2107. // Execute callback on different thread
  2108. ThreadAbstraction.ExecuteThread(() => listCallback(result.Count));
  2109. }
  2110. files = _sftpSession.RequestReadDir(handle);
  2111. }
  2112. _sftpSession.RequestClose(handle);
  2113. return result;
  2114. }
  2115. /// <summary>
  2116. /// Internals the download file.
  2117. /// </summary>
  2118. /// <param name="path">The path.</param>
  2119. /// <param name="output">The output.</param>
  2120. /// <param name="asyncResult">An <see cref="IAsyncResult"/> that references the asynchronous request.</param>
  2121. /// <param name="downloadCallback">The download callback.</param>
  2122. /// <exception cref="ArgumentNullException"><paramref name="output" /> is <see langword="null"/>.</exception>
  2123. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains whitespace.</exception>
  2124. /// <exception cref="SshConnectionException">Client not connected.</exception>
  2125. private void InternalDownloadFile(string path, Stream output, SftpDownloadAsyncResult? asyncResult, Action<ulong>? downloadCallback)
  2126. {
  2127. ThrowHelper.ThrowIfNull(output);
  2128. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  2129. if (_sftpSession is null)
  2130. {
  2131. throw new SshConnectionException("Client not connected.");
  2132. }
  2133. var fullPath = _sftpSession.GetCanonicalPath(path);
  2134. using (var fileReader = ServiceFactory.CreateSftpFileReader(fullPath, _sftpSession, _bufferSize))
  2135. {
  2136. var totalBytesRead = 0UL;
  2137. while (true)
  2138. {
  2139. // Cancel download
  2140. if (asyncResult is not null && asyncResult.IsDownloadCanceled)
  2141. {
  2142. break;
  2143. }
  2144. var data = fileReader.Read();
  2145. if (data.Length == 0)
  2146. {
  2147. break;
  2148. }
  2149. output.Write(data, 0, data.Length);
  2150. totalBytesRead += (ulong)data.Length;
  2151. asyncResult?.Update(totalBytesRead);
  2152. if (downloadCallback is not null)
  2153. {
  2154. // Copy offset to ensure it's not modified between now and execution of callback
  2155. var downloadOffset = totalBytesRead;
  2156. // Execute callback on different thread
  2157. ThreadAbstraction.ExecuteThread(() => { downloadCallback(downloadOffset); });
  2158. }
  2159. }
  2160. }
  2161. }
  2162. /// <summary>
  2163. /// Internals the upload file.
  2164. /// </summary>
  2165. /// <param name="input">The input.</param>
  2166. /// <param name="path">The path.</param>
  2167. /// <param name="flags">The flags.</param>
  2168. /// <param name="asyncResult">An <see cref="IAsyncResult"/> that references the asynchronous request.</param>
  2169. /// <param name="uploadCallback">The upload callback.</param>
  2170. /// <exception cref="ArgumentNullException"><paramref name="input" /> is <see langword="null"/>.</exception>
  2171. /// <exception cref="ArgumentException"><paramref name="path" /> is <see langword="null"/> or contains whitespace.</exception>
  2172. /// <exception cref="SshConnectionException">Client not connected.</exception>
  2173. private void InternalUploadFile(Stream input, string path, Flags flags, SftpUploadAsyncResult? asyncResult, Action<ulong>? uploadCallback)
  2174. {
  2175. ThrowHelper.ThrowIfNull(input);
  2176. ThrowHelper.ThrowIfNullOrWhiteSpace(path);
  2177. if (_sftpSession is null)
  2178. {
  2179. throw new SshConnectionException("Client not connected.");
  2180. }
  2181. var fullPath = _sftpSession.GetCanonicalPath(path);
  2182. var handle = _sftpSession.RequestOpen(fullPath, flags);
  2183. ulong offset = 0;
  2184. // create buffer of optimal length
  2185. var buffer = new byte[_sftpSession.CalculateOptimalWriteLength(_bufferSize, handle)];
  2186. var bytesRead = input.Read(buffer, 0, buffer.Length);
  2187. var expectedResponses = 0;
  2188. var responseReceivedWaitHandle = new AutoResetEvent(initialState: false);
  2189. do
  2190. {
  2191. // Cancel upload
  2192. if (asyncResult is not null && asyncResult.IsUploadCanceled)
  2193. {
  2194. break;
  2195. }
  2196. if (bytesRead > 0)
  2197. {
  2198. var writtenBytes = offset + (ulong)bytesRead;
  2199. _sftpSession.RequestWrite(handle, offset, buffer, offset: 0, bytesRead, wait: null, s =>
  2200. {
  2201. if (s.StatusCode == StatusCodes.Ok)
  2202. {
  2203. _ = Interlocked.Decrement(ref expectedResponses);
  2204. _ = responseReceivedWaitHandle.Set();
  2205. asyncResult?.Update(writtenBytes);
  2206. // Call callback to report number of bytes written
  2207. if (uploadCallback is not null)
  2208. {
  2209. // Execute callback on different thread
  2210. ThreadAbstraction.ExecuteThread(() => uploadCallback(writtenBytes));
  2211. }
  2212. }
  2213. });
  2214. _ = Interlocked.Increment(ref expectedResponses);
  2215. offset += (ulong)bytesRead;
  2216. bytesRead = input.Read(buffer, 0, buffer.Length);
  2217. }
  2218. else if (expectedResponses > 0)
  2219. {
  2220. // Wait for expectedResponses to change
  2221. _sftpSession.WaitOnHandle(responseReceivedWaitHandle, _operationTimeout);
  2222. }
  2223. }
  2224. while (expectedResponses > 0 || bytesRead > 0);
  2225. _sftpSession.RequestClose(handle);
  2226. responseReceivedWaitHandle.Dispose();
  2227. }
  2228. /// <summary>
  2229. /// Called when client is connected to the server.
  2230. /// </summary>
  2231. protected override void OnConnected()
  2232. {
  2233. base.OnConnected();
  2234. _sftpSession?.Dispose();
  2235. _sftpSession = CreateAndConnectToSftpSession();
  2236. }
  2237. /// <summary>
  2238. /// Called when client is disconnecting from the server.
  2239. /// </summary>
  2240. protected override void OnDisconnecting()
  2241. {
  2242. base.OnDisconnecting();
  2243. // disconnect, dispose and dereference the SFTP session since we create a new SFTP session
  2244. // on each connect
  2245. var sftpSession = _sftpSession;
  2246. if (sftpSession is not null)
  2247. {
  2248. _sftpSession = null;
  2249. sftpSession.Dispose();
  2250. }
  2251. }
  2252. /// <summary>
  2253. /// Releases unmanaged and - optionally - managed resources.
  2254. /// </summary>
  2255. /// <param name="disposing"><see langword="true"/> to release both managed and unmanaged resources; <see langword="false"/> to release only unmanaged resources.</param>
  2256. protected override void Dispose(bool disposing)
  2257. {
  2258. base.Dispose(disposing);
  2259. if (disposing)
  2260. {
  2261. var sftpSession = _sftpSession;
  2262. if (sftpSession is not null)
  2263. {
  2264. _sftpSession = null;
  2265. sftpSession.Dispose();
  2266. }
  2267. }
  2268. }
  2269. private ISftpSession CreateAndConnectToSftpSession()
  2270. {
  2271. var sftpSession = ServiceFactory.CreateSftpSession(Session,
  2272. _operationTimeout,
  2273. ConnectionInfo.Encoding,
  2274. ServiceFactory.CreateSftpResponseFactory());
  2275. try
  2276. {
  2277. sftpSession.Connect();
  2278. return sftpSession;
  2279. }
  2280. catch
  2281. {
  2282. sftpSession.Dispose();
  2283. throw;
  2284. }
  2285. }
  2286. }
  2287. }