SftpSession.cs 100 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Text;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Renci.SshNet.Common;
  8. using Renci.SshNet.Sftp.Requests;
  9. using Renci.SshNet.Sftp.Responses;
  10. namespace Renci.SshNet.Sftp
  11. {
  12. /// <summary>
  13. /// Represents an SFTP session.
  14. /// </summary>
  15. internal sealed class SftpSession : SubsystemSession, ISftpSession
  16. {
  17. internal const int MaximumSupportedVersion = 3;
  18. private const int MinimumSupportedVersion = 0;
  19. private readonly Dictionary<uint, SftpRequest> _requests = new Dictionary<uint, SftpRequest>();
  20. private readonly ISftpResponseFactory _sftpResponseFactory;
  21. private readonly List<byte> _data = new List<byte>(32 * 1024);
  22. private readonly Encoding _encoding;
  23. private EventWaitHandle _sftpVersionConfirmed = new AutoResetEvent(initialState: false);
  24. private IDictionary<string, string> _supportedExtensions;
  25. /// <summary>
  26. /// Gets the remote working directory.
  27. /// </summary>
  28. /// <value>
  29. /// The remote working directory.
  30. /// </value>
  31. public string WorkingDirectory { get; private set; }
  32. /// <summary>
  33. /// Gets the SFTP protocol version.
  34. /// </summary>
  35. /// <value>
  36. /// The SFTP protocol version.
  37. /// </value>
  38. public uint ProtocolVersion { get; private set; }
  39. private long _requestId;
  40. /// <summary>
  41. /// Gets the next request id for sftp session.
  42. /// </summary>
  43. public uint NextRequestId
  44. {
  45. get
  46. {
  47. return (uint)Interlocked.Increment(ref _requestId);
  48. }
  49. }
  50. /// <summary>
  51. /// Initializes a new instance of the <see cref="SftpSession"/> class.
  52. /// </summary>
  53. /// <param name="session">The SSH session.</param>
  54. /// <param name="operationTimeout">The operation timeout.</param>
  55. /// <param name="encoding">The character encoding to use.</param>
  56. /// <param name="sftpResponseFactory">The factory to create SFTP responses.</param>
  57. public SftpSession(ISession session, int operationTimeout, Encoding encoding, ISftpResponseFactory sftpResponseFactory)
  58. : base(session, "sftp", operationTimeout)
  59. {
  60. _encoding = encoding;
  61. _sftpResponseFactory = sftpResponseFactory;
  62. }
  63. /// <summary>
  64. /// Changes the current working directory to the specified path.
  65. /// </summary>
  66. /// <param name="path">The new working directory.</param>
  67. public void ChangeDirectory(string path)
  68. {
  69. var fullPath = GetCanonicalPath(path);
  70. var handle = RequestOpenDir(fullPath);
  71. RequestClose(handle);
  72. WorkingDirectory = fullPath;
  73. }
  74. internal void SendMessage(SftpMessage sftpMessage)
  75. {
  76. var data = sftpMessage.GetBytes();
  77. SendData(data);
  78. }
  79. /// <summary>
  80. /// Resolves a given path into an absolute path on the server.
  81. /// </summary>
  82. /// <param name="path">The path to resolve.</param>
  83. /// <returns>
  84. /// The absolute path.
  85. /// </returns>
  86. public string GetCanonicalPath(string path)
  87. {
  88. var fullPath = GetFullRemotePath(path);
  89. var canonizedPath = string.Empty;
  90. var realPathFiles = RequestRealPath(fullPath, nullOnError: true);
  91. if (realPathFiles != null)
  92. {
  93. canonizedPath = realPathFiles[0].Key;
  94. }
  95. if (!string.IsNullOrEmpty(canonizedPath))
  96. {
  97. return canonizedPath;
  98. }
  99. // Check for special cases
  100. if (fullPath.EndsWith("/.", StringComparison.OrdinalIgnoreCase) ||
  101. fullPath.EndsWith("/..", StringComparison.OrdinalIgnoreCase) ||
  102. fullPath.Equals("/", StringComparison.OrdinalIgnoreCase) ||
  103. #if NET || NETSTANDARD2_1_OR_GREATER
  104. fullPath.IndexOf('/', StringComparison.OrdinalIgnoreCase) < 0)
  105. #else
  106. fullPath.IndexOf('/') < 0)
  107. #endif // NET || NETSTANDARD2_1_OR_GREATER
  108. {
  109. return fullPath;
  110. }
  111. var pathParts = fullPath.Split('/');
  112. #if NET || NETSTANDARD2_1_OR_GREATER
  113. var partialFullPath = string.Join('/', pathParts, 0, pathParts.Length - 1);
  114. #else
  115. var partialFullPath = string.Join("/", pathParts, 0, pathParts.Length - 1);
  116. #endif // NET || NETSTANDARD2_1_OR_GREATER
  117. if (string.IsNullOrEmpty(partialFullPath))
  118. {
  119. partialFullPath = "/";
  120. }
  121. realPathFiles = RequestRealPath(partialFullPath, nullOnError: true);
  122. if (realPathFiles != null)
  123. {
  124. canonizedPath = realPathFiles[0].Key;
  125. }
  126. if (string.IsNullOrEmpty(canonizedPath))
  127. {
  128. return fullPath;
  129. }
  130. var slash = string.Empty;
  131. if (canonizedPath[canonizedPath.Length - 1] != '/')
  132. {
  133. slash = "/";
  134. }
  135. return string.Format(CultureInfo.InvariantCulture, "{0}{1}{2}", canonizedPath, slash, pathParts[pathParts.Length - 1]);
  136. }
  137. /// <summary>
  138. /// Asynchronously resolves a given path into an absolute path on the server.
  139. /// </summary>
  140. /// <param name="path">The path to resolve.</param>
  141. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  142. /// <returns>
  143. /// A task representing the absolute path.
  144. /// </returns>
  145. public async Task<string> GetCanonicalPathAsync(string path, CancellationToken cancellationToken)
  146. {
  147. var fullPath = GetFullRemotePath(path);
  148. var canonizedPath = string.Empty;
  149. var realPathFiles = await RequestRealPathAsync(fullPath, nullOnError: true, cancellationToken).ConfigureAwait(false);
  150. if (realPathFiles != null)
  151. {
  152. canonizedPath = realPathFiles[0].Key;
  153. }
  154. if (!string.IsNullOrEmpty(canonizedPath))
  155. {
  156. return canonizedPath;
  157. }
  158. // Check for special cases
  159. if (fullPath.EndsWith("/.", StringComparison.Ordinal) ||
  160. fullPath.EndsWith("/..", StringComparison.Ordinal) ||
  161. fullPath.Equals("/", StringComparison.Ordinal) ||
  162. #if NET || NETSTANDARD2_1_OR_GREATER
  163. fullPath.IndexOf('/', StringComparison.Ordinal) < 0)
  164. #else
  165. fullPath.IndexOf('/') < 0)
  166. #endif // NET || NETSTANDARD2_1_OR_GREATER
  167. {
  168. return fullPath;
  169. }
  170. var pathParts = fullPath.Split('/');
  171. #if NET || NETSTANDARD2_1_OR_GREATER
  172. var partialFullPath = string.Join('/', pathParts);
  173. #else
  174. var partialFullPath = string.Join("/", pathParts);
  175. #endif // NET || NETSTANDARD2_1_OR_GREATER
  176. if (string.IsNullOrEmpty(partialFullPath))
  177. {
  178. partialFullPath = "/";
  179. }
  180. realPathFiles = await RequestRealPathAsync(partialFullPath, nullOnError: true, cancellationToken).ConfigureAwait(false);
  181. if (realPathFiles != null)
  182. {
  183. canonizedPath = realPathFiles[0].Key;
  184. }
  185. if (string.IsNullOrEmpty(canonizedPath))
  186. {
  187. return fullPath;
  188. }
  189. var slash = string.Empty;
  190. if (canonizedPath[canonizedPath.Length - 1] != '/')
  191. {
  192. slash = "/";
  193. }
  194. return canonizedPath + slash + pathParts[pathParts.Length - 1];
  195. }
  196. /// <summary>
  197. /// Creates an <see cref="ISftpFileReader"/> for reading the content of the file represented by a given <paramref name="handle"/>.
  198. /// </summary>
  199. /// <param name="handle">The handle of the file to read.</param>
  200. /// <param name="sftpSession">The SFTP session.</param>
  201. /// <param name="chunkSize">The maximum number of bytes to read with each chunk.</param>
  202. /// <param name="maxPendingReads">The maximum number of pending reads.</param>
  203. /// <param name="fileSize">The size of the file or <see langword="null"/> when the size could not be determined.</param>
  204. /// <returns>
  205. /// An <see cref="ISftpFileReader"/> for reading the content of the file represented by the
  206. /// specified <paramref name="handle"/>.
  207. /// </returns>
  208. public ISftpFileReader CreateFileReader(byte[] handle, ISftpSession sftpSession, uint chunkSize, int maxPendingReads, long? fileSize)
  209. {
  210. return new SftpFileReader(handle, sftpSession, chunkSize, maxPendingReads, fileSize);
  211. }
  212. internal string GetFullRemotePath(string path)
  213. {
  214. var fullPath = path;
  215. if (!string.IsNullOrEmpty(path) && path[0] != '/' && WorkingDirectory != null)
  216. {
  217. if (WorkingDirectory[WorkingDirectory.Length - 1] == '/')
  218. {
  219. fullPath = WorkingDirectory + path;
  220. }
  221. else
  222. {
  223. fullPath = WorkingDirectory + '/' + path;
  224. }
  225. }
  226. return fullPath;
  227. }
  228. protected override void OnChannelOpen()
  229. {
  230. SendMessage(new SftpInitRequest(MaximumSupportedVersion));
  231. WaitOnHandle(_sftpVersionConfirmed, OperationTimeout);
  232. if (ProtocolVersion is > MaximumSupportedVersion or < MinimumSupportedVersion)
  233. {
  234. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Server SFTP version {0} is not supported.", ProtocolVersion));
  235. }
  236. // Resolve current directory
  237. WorkingDirectory = RequestRealPath(".")[0].Key;
  238. }
  239. protected override void OnDataReceived(byte[] data)
  240. {
  241. const int packetLengthByteCount = 4;
  242. const int sftpMessageTypeByteCount = 1;
  243. const int minimumChannelDataLength = packetLengthByteCount + sftpMessageTypeByteCount;
  244. var offset = 0;
  245. var count = data.Length;
  246. // improve performance and reduce GC pressure by not buffering channel data if the received
  247. // chunk contains the complete packet data.
  248. //
  249. // for this, the buffer should be empty and the chunk should contain at least the packet length
  250. // and the type of the SFTP message
  251. if (_data.Count == 0)
  252. {
  253. while (count >= minimumChannelDataLength)
  254. {
  255. // extract packet length
  256. var packetDataLength = data[offset] << 24 | data[offset + 1] << 16 | data[offset + 2] << 8 |
  257. data[offset + 3];
  258. var packetTotalLength = packetDataLength + packetLengthByteCount;
  259. // check if complete packet data (or more) is available
  260. if (count >= packetTotalLength)
  261. {
  262. // load and process SFTP message
  263. if (!TryLoadSftpMessage(data, offset + packetLengthByteCount, packetDataLength))
  264. {
  265. return;
  266. }
  267. // remove processed bytes from the number of bytes to process as the channel
  268. // data we received may contain (part of) another message
  269. count -= packetTotalLength;
  270. // move offset beyond bytes we just processed
  271. offset += packetTotalLength;
  272. }
  273. else
  274. {
  275. // we don't have a complete message
  276. break;
  277. }
  278. }
  279. // check if there is channel data left to process or buffer
  280. if (count == 0)
  281. {
  282. return;
  283. }
  284. // check if we processed part of the channel data we received
  285. if (offset > 0)
  286. {
  287. // add (remaining) channel data to internal data holder
  288. var remainingChannelData = new byte[count];
  289. Buffer.BlockCopy(data, offset, remainingChannelData, 0, count);
  290. _data.AddRange(remainingChannelData);
  291. }
  292. else
  293. {
  294. // add (remaining) channel data to internal data holder
  295. _data.AddRange(data);
  296. }
  297. // skip further processing as we'll need a new chunk to complete the message
  298. return;
  299. }
  300. // add (remaining) channel data to internal data holder
  301. _data.AddRange(data);
  302. while (_data.Count >= minimumChannelDataLength)
  303. {
  304. // extract packet length
  305. var packetDataLength = _data[0] << 24 | _data[1] << 16 | _data[2] << 8 | _data[3];
  306. var packetTotalLength = packetDataLength + packetLengthByteCount;
  307. // check if complete packet data is available
  308. if (_data.Count < packetTotalLength)
  309. {
  310. // wait for complete message to arrive first
  311. break;
  312. }
  313. // create buffer to hold packet data
  314. var packetData = new byte[packetDataLength];
  315. // copy packet data and bytes for length to array
  316. _data.CopyTo(packetLengthByteCount, packetData, 0, packetDataLength);
  317. // remove loaded data and bytes for length from _data holder
  318. if (_data.Count == packetTotalLength)
  319. {
  320. // the only buffered data is the data we're processing
  321. _data.Clear();
  322. }
  323. else
  324. {
  325. // remove only the data we're processing
  326. _data.RemoveRange(0, packetTotalLength);
  327. }
  328. // load and process SFTP message
  329. if (!TryLoadSftpMessage(packetData, 0, packetDataLength))
  330. {
  331. break;
  332. }
  333. }
  334. }
  335. private bool TryLoadSftpMessage(byte[] packetData, int offset, int count)
  336. {
  337. // Create SFTP message
  338. var response = _sftpResponseFactory.Create(ProtocolVersion, packetData[offset], _encoding);
  339. // Load message data into it
  340. response.Load(packetData, offset + 1, count - 1);
  341. try
  342. {
  343. if (response is SftpVersionResponse versionResponse)
  344. {
  345. ProtocolVersion = versionResponse.Version;
  346. _supportedExtensions = versionResponse.Extentions;
  347. _ = _sftpVersionConfirmed.Set();
  348. }
  349. else
  350. {
  351. HandleResponse(response as SftpResponse);
  352. }
  353. return true;
  354. }
  355. catch (Exception exp)
  356. {
  357. RaiseError(exp);
  358. return false;
  359. }
  360. }
  361. protected override void Dispose(bool disposing)
  362. {
  363. base.Dispose(disposing);
  364. if (disposing)
  365. {
  366. var sftpVersionConfirmed = _sftpVersionConfirmed;
  367. if (sftpVersionConfirmed != null)
  368. {
  369. _sftpVersionConfirmed = null;
  370. sftpVersionConfirmed.Dispose();
  371. }
  372. }
  373. }
  374. private void SendRequest(SftpRequest request)
  375. {
  376. lock (_requests)
  377. {
  378. _requests.Add(request.RequestId, request);
  379. }
  380. SendMessage(request);
  381. }
  382. /// <summary>
  383. /// Performs SSH_FXP_OPEN request.
  384. /// </summary>
  385. /// <param name="path">The path.</param>
  386. /// <param name="flags">The flags.</param>
  387. /// <param name="nullOnError">If set to <see langword="true"/> returns <see langword="null"/> instead of throwing an exception.</param>
  388. /// <returns>File handle.</returns>
  389. public byte[] RequestOpen(string path, Flags flags, bool nullOnError = false)
  390. {
  391. byte[] handle = null;
  392. SshException exception = null;
  393. using (var wait = new AutoResetEvent(initialState: false))
  394. {
  395. var request = new SftpOpenRequest(ProtocolVersion,
  396. NextRequestId,
  397. path,
  398. _encoding,
  399. flags,
  400. response =>
  401. {
  402. handle = response.Handle;
  403. _ = wait.Set();
  404. },
  405. response =>
  406. {
  407. exception = GetSftpException(response);
  408. _ = wait.Set();
  409. });
  410. SendRequest(request);
  411. WaitOnHandle(wait, OperationTimeout);
  412. }
  413. if (!nullOnError && exception is not null)
  414. {
  415. throw exception;
  416. }
  417. return handle;
  418. }
  419. /// <summary>
  420. /// Asynchronously performs a <c>SSH_FXP_OPEN</c> request.
  421. /// </summary>
  422. /// <param name="path">The path.</param>
  423. /// <param name="flags">The flags.</param>
  424. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  425. /// <returns>
  426. /// A task that represents the asynchronous <c>SSH_FXP_OPEN</c> request. The value of its
  427. /// <see cref="Task{Task}.Result"/> contains the file handle of the specified path.
  428. /// </returns>
  429. public async Task<byte[]> RequestOpenAsync(string path, Flags flags, CancellationToken cancellationToken)
  430. {
  431. cancellationToken.ThrowIfCancellationRequested();
  432. var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
  433. #if NET || NETSTANDARD2_1_OR_GREATER
  434. await using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  435. #else
  436. using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  437. #endif // NET || NETSTANDARD2_1_OR_GREATER
  438. {
  439. SendRequest(new SftpOpenRequest(ProtocolVersion,
  440. NextRequestId,
  441. path,
  442. _encoding,
  443. flags,
  444. response => tcs.TrySetResult(response.Handle),
  445. response => tcs.TrySetException(GetSftpException(response))));
  446. return await tcs.Task.ConfigureAwait(false);
  447. }
  448. }
  449. /// <summary>
  450. /// Performs SSH_FXP_OPEN request.
  451. /// </summary>
  452. /// <param name="path">The path.</param>
  453. /// <param name="flags">The flags.</param>
  454. /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginOpen(string, Flags, AsyncCallback, object)"/> completes.</param>
  455. /// <param name="state">An object that contains any additional user-defined data.</param>
  456. /// <returns>
  457. /// A <see cref="SftpOpenAsyncResult"/> that represents the asynchronous call.
  458. /// </returns>
  459. public SftpOpenAsyncResult BeginOpen(string path, Flags flags, AsyncCallback callback, object state)
  460. {
  461. var asyncResult = new SftpOpenAsyncResult(callback, state);
  462. var request = new SftpOpenRequest(ProtocolVersion,
  463. NextRequestId,
  464. path,
  465. _encoding,
  466. flags,
  467. response =>
  468. {
  469. asyncResult.SetAsCompleted(response.Handle, completedSynchronously: false);
  470. },
  471. response =>
  472. {
  473. asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
  474. });
  475. SendRequest(request);
  476. return asyncResult;
  477. }
  478. /// <summary>
  479. /// Handles the end of an asynchronous open.
  480. /// </summary>
  481. /// <param name="asyncResult">An <see cref="SftpOpenAsyncResult"/> that represents an asynchronous call.</param>
  482. /// <returns>
  483. /// A <see cref="byte"/> array representing a file handle.
  484. /// </returns>
  485. /// <remarks>
  486. /// If all available data has been read, the <see cref="EndOpen(SftpOpenAsyncResult)"/> method completes
  487. /// immediately and returns zero bytes.
  488. /// </remarks>
  489. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  490. public byte[] EndOpen(SftpOpenAsyncResult asyncResult)
  491. {
  492. ThrowHelper.ThrowIfNull(asyncResult);
  493. if (asyncResult.EndInvokeCalled)
  494. {
  495. throw new InvalidOperationException("EndOpen has already been called.");
  496. }
  497. if (asyncResult.IsCompleted)
  498. {
  499. return asyncResult.EndInvoke();
  500. }
  501. using (var waitHandle = asyncResult.AsyncWaitHandle)
  502. {
  503. WaitOnHandle(waitHandle, OperationTimeout);
  504. return asyncResult.EndInvoke();
  505. }
  506. }
  507. /// <summary>
  508. /// Performs SSH_FXP_CLOSE request.
  509. /// </summary>
  510. /// <param name="handle">The handle.</param>
  511. public void RequestClose(byte[] handle)
  512. {
  513. SshException exception = null;
  514. using (var wait = new AutoResetEvent(initialState: false))
  515. {
  516. var request = new SftpCloseRequest(ProtocolVersion,
  517. NextRequestId,
  518. handle,
  519. response =>
  520. {
  521. exception = GetSftpException(response);
  522. _ = wait.Set();
  523. });
  524. SendRequest(request);
  525. WaitOnHandle(wait, OperationTimeout);
  526. }
  527. if (exception is not null)
  528. {
  529. throw exception;
  530. }
  531. }
  532. /// <summary>
  533. /// Performs a <c>SSH_FXP_CLOSE</c> request.
  534. /// </summary>
  535. /// <param name="handle">The handle.</param>
  536. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  537. /// <returns>
  538. /// A task that represents the asynchronous <c>SSH_FXP_CLOSE</c> request.
  539. /// </returns>
  540. public async Task RequestCloseAsync(byte[] handle, CancellationToken cancellationToken)
  541. {
  542. var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
  543. SendRequest(new SftpCloseRequest(ProtocolVersion,
  544. NextRequestId,
  545. handle,
  546. response =>
  547. {
  548. if (response.StatusCode == StatusCodes.Ok)
  549. {
  550. _ = tcs.TrySetResult(true);
  551. }
  552. else
  553. {
  554. _ = tcs.TrySetException(GetSftpException(response));
  555. }
  556. }));
  557. // Only check for cancellation after the SftpCloseRequest was sent
  558. cancellationToken.ThrowIfCancellationRequested();
  559. #if NET || NETSTANDARD2_1_OR_GREATER
  560. await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  561. #else
  562. using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  563. #endif // NET || NETSTANDARD2_1_OR_GREATER
  564. {
  565. _ = await tcs.Task.ConfigureAwait(false);
  566. }
  567. }
  568. /// <summary>
  569. /// Performs SSH_FXP_CLOSE request.
  570. /// </summary>
  571. /// <param name="handle">The handle.</param>
  572. /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginClose(byte[], AsyncCallback, object)"/> completes.</param>
  573. /// <param name="state">An object that contains any additional user-defined data.</param>
  574. /// <returns>
  575. /// A <see cref="SftpCloseAsyncResult"/> that represents the asynchronous call.
  576. /// </returns>
  577. public SftpCloseAsyncResult BeginClose(byte[] handle, AsyncCallback callback, object state)
  578. {
  579. var asyncResult = new SftpCloseAsyncResult(callback, state);
  580. var request = new SftpCloseRequest(ProtocolVersion,
  581. NextRequestId,
  582. handle,
  583. response =>
  584. {
  585. asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
  586. });
  587. SendRequest(request);
  588. return asyncResult;
  589. }
  590. /// <summary>
  591. /// Handles the end of an asynchronous close.
  592. /// </summary>
  593. /// <param name="asyncResult">An <see cref="SftpCloseAsyncResult"/> that represents an asynchronous call.</param>
  594. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  595. public void EndClose(SftpCloseAsyncResult asyncResult)
  596. {
  597. ThrowHelper.ThrowIfNull(asyncResult);
  598. if (asyncResult.EndInvokeCalled)
  599. {
  600. throw new InvalidOperationException("EndClose has already been called.");
  601. }
  602. if (asyncResult.IsCompleted)
  603. {
  604. asyncResult.EndInvoke();
  605. }
  606. else
  607. {
  608. using (var waitHandle = asyncResult.AsyncWaitHandle)
  609. {
  610. WaitOnHandle(waitHandle, OperationTimeout);
  611. asyncResult.EndInvoke();
  612. }
  613. }
  614. }
  615. /// <summary>
  616. /// Begins an asynchronous read using a SSH_FXP_READ request.
  617. /// </summary>
  618. /// <param name="handle">The handle to the file to read from.</param>
  619. /// <param name="offset">The offset in the file to start reading from.</param>
  620. /// <param name="length">The number of bytes to read.</param>
  621. /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginRead(byte[], ulong, uint, AsyncCallback, object)"/> completes.</param>
  622. /// <param name="state">An object that contains any additional user-defined data.</param>
  623. /// <returns>
  624. /// A <see cref="SftpReadAsyncResult"/> that represents the asynchronous call.
  625. /// </returns>
  626. public SftpReadAsyncResult BeginRead(byte[] handle, ulong offset, uint length, AsyncCallback callback, object state)
  627. {
  628. var asyncResult = new SftpReadAsyncResult(callback, state);
  629. var request = new SftpReadRequest(ProtocolVersion,
  630. NextRequestId,
  631. handle,
  632. offset,
  633. length,
  634. response =>
  635. {
  636. asyncResult.SetAsCompleted(response.Data, completedSynchronously: false);
  637. },
  638. response =>
  639. {
  640. if (response.StatusCode != StatusCodes.Eof)
  641. {
  642. asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
  643. }
  644. else
  645. {
  646. asyncResult.SetAsCompleted(Array.Empty<byte>(), completedSynchronously: false);
  647. }
  648. });
  649. SendRequest(request);
  650. return asyncResult;
  651. }
  652. /// <summary>
  653. /// Handles the end of an asynchronous read.
  654. /// </summary>
  655. /// <param name="asyncResult">An <see cref="SftpReadAsyncResult"/> that represents an asynchronous call.</param>
  656. /// <returns>
  657. /// A <see cref="byte"/> array representing the data read.
  658. /// </returns>
  659. /// <remarks>
  660. /// If all available data has been read, the <see cref="EndRead(SftpReadAsyncResult)"/> method completes
  661. /// immediately and returns zero bytes.
  662. /// </remarks>
  663. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  664. public byte[] EndRead(SftpReadAsyncResult asyncResult)
  665. {
  666. ThrowHelper.ThrowIfNull(asyncResult);
  667. if (asyncResult.EndInvokeCalled)
  668. {
  669. throw new InvalidOperationException("EndRead has already been called.");
  670. }
  671. if (asyncResult.IsCompleted)
  672. {
  673. return asyncResult.EndInvoke();
  674. }
  675. using (var waitHandle = asyncResult.AsyncWaitHandle)
  676. {
  677. WaitOnHandle(waitHandle, OperationTimeout);
  678. return asyncResult.EndInvoke();
  679. }
  680. }
  681. /// <summary>
  682. /// Performs SSH_FXP_READ request.
  683. /// </summary>
  684. /// <param name="handle">The handle.</param>
  685. /// <param name="offset">The offset.</param>
  686. /// <param name="length">The length.</param>
  687. /// <returns>
  688. /// The data that was read, or an empty array when the end of the file was reached.
  689. /// </returns>
  690. public byte[] RequestRead(byte[] handle, ulong offset, uint length)
  691. {
  692. SshException exception = null;
  693. byte[] data = null;
  694. using (var wait = new AutoResetEvent(initialState: false))
  695. {
  696. var request = new SftpReadRequest(ProtocolVersion,
  697. NextRequestId,
  698. handle,
  699. offset,
  700. length,
  701. response =>
  702. {
  703. data = response.Data;
  704. _ = wait.Set();
  705. },
  706. response =>
  707. {
  708. if (response.StatusCode != StatusCodes.Eof)
  709. {
  710. exception = GetSftpException(response);
  711. }
  712. else
  713. {
  714. data = Array.Empty<byte>();
  715. }
  716. _ = wait.Set();
  717. });
  718. SendRequest(request);
  719. WaitOnHandle(wait, OperationTimeout);
  720. }
  721. if (exception is not null)
  722. {
  723. throw exception;
  724. }
  725. return data;
  726. }
  727. /// <summary>
  728. /// Asynchronously performs a <c>SSH_FXP_READ</c> request.
  729. /// </summary>
  730. /// <param name="handle">The handle to the file to read from.</param>
  731. /// <param name="offset">The offset in the file to start reading from.</param>
  732. /// <param name="length">The number of bytes to read.</param>
  733. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  734. /// <returns>
  735. /// A task that represents the asynchronous <c>SSH_FXP_READ</c> request. The value of
  736. /// its <see cref="Task{Task}.Result"/> contains the data read from the file, or an empty
  737. /// array when the end of the file is reached.
  738. /// </returns>
  739. public async Task<byte[]> RequestReadAsync(byte[] handle, ulong offset, uint length, CancellationToken cancellationToken)
  740. {
  741. cancellationToken.ThrowIfCancellationRequested();
  742. var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
  743. #if NET || NETSTANDARD2_1_OR_GREATER
  744. await using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  745. #else
  746. using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  747. #endif // NET || NETSTANDARD2_1_OR_GREATER
  748. {
  749. SendRequest(new SftpReadRequest(ProtocolVersion,
  750. NextRequestId,
  751. handle,
  752. offset,
  753. length,
  754. response => tcs.TrySetResult(response.Data),
  755. response =>
  756. {
  757. if (response.StatusCode == StatusCodes.Eof)
  758. {
  759. _ = tcs.TrySetResult(Array.Empty<byte>());
  760. }
  761. else
  762. {
  763. _ = tcs.TrySetException(GetSftpException(response));
  764. }
  765. }));
  766. return await tcs.Task.ConfigureAwait(false);
  767. }
  768. }
  769. /// <summary>
  770. /// Performs SSH_FXP_WRITE request.
  771. /// </summary>
  772. /// <param name="handle">The handle.</param>
  773. /// <param name="serverOffset">The the zero-based offset (in bytes) relative to the beginning of the file that the write must start at.</param>
  774. /// <param name="data">The buffer holding the data to write.</param>
  775. /// <param name="offset">the zero-based offset in <paramref name="data" /> at which to begin taking bytes to write.</param>
  776. /// <param name="length">The length (in bytes) of the data to write.</param>
  777. /// <param name="wait">The wait event handle if needed.</param>
  778. /// <param name="writeCompleted">The callback to invoke when the write has completed.</param>
  779. public void RequestWrite(byte[] handle,
  780. ulong serverOffset,
  781. byte[] data,
  782. int offset,
  783. int length,
  784. AutoResetEvent wait,
  785. Action<SftpStatusResponse> writeCompleted = null)
  786. {
  787. SshException exception = null;
  788. var request = new SftpWriteRequest(ProtocolVersion,
  789. NextRequestId,
  790. handle,
  791. serverOffset,
  792. data,
  793. offset,
  794. length,
  795. response =>
  796. {
  797. writeCompleted?.Invoke(response);
  798. exception = GetSftpException(response);
  799. if (wait != null)
  800. {
  801. _ = wait.Set();
  802. }
  803. });
  804. SendRequest(request);
  805. if (wait is not null)
  806. {
  807. WaitOnHandle(wait, OperationTimeout);
  808. }
  809. if (exception is not null)
  810. {
  811. throw exception;
  812. }
  813. }
  814. /// <summary>
  815. /// Asynchronouly performs a <c>SSH_FXP_WRITE</c> request.
  816. /// </summary>
  817. /// <param name="handle">The handle.</param>
  818. /// <param name="serverOffset">The the zero-based offset (in bytes) relative to the beginning of the file that the write must start at.</param>
  819. /// <param name="data">The buffer holding the data to write.</param>
  820. /// <param name="offset">the zero-based offset in <paramref name="data" /> at which to begin taking bytes to write.</param>
  821. /// <param name="length">The length (in bytes) of the data to write.</param>
  822. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  823. /// <returns>
  824. /// A task that represents the asynchronous <c>SSH_FXP_WRITE</c> request.
  825. /// </returns>
  826. public async Task RequestWriteAsync(byte[] handle, ulong serverOffset, byte[] data, int offset, int length, CancellationToken cancellationToken)
  827. {
  828. cancellationToken.ThrowIfCancellationRequested();
  829. var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
  830. #if NET || NETSTANDARD2_1_OR_GREATER
  831. await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  832. #else
  833. using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  834. #endif // NET || NETSTANDARD2_1_OR_GREATER
  835. {
  836. SendRequest(new SftpWriteRequest(ProtocolVersion,
  837. NextRequestId,
  838. handle,
  839. serverOffset,
  840. data,
  841. offset,
  842. length,
  843. response =>
  844. {
  845. if (response.StatusCode == StatusCodes.Ok)
  846. {
  847. _ = tcs.TrySetResult(true);
  848. }
  849. else
  850. {
  851. _ = tcs.TrySetException(GetSftpException(response));
  852. }
  853. }));
  854. _ = await tcs.Task.ConfigureAwait(false);
  855. }
  856. }
  857. /// <summary>
  858. /// Performs SSH_FXP_LSTAT request.
  859. /// </summary>
  860. /// <param name="path">The path.</param>
  861. /// <returns>
  862. /// File attributes.
  863. /// </returns>
  864. public SftpFileAttributes RequestLStat(string path)
  865. {
  866. SshException exception = null;
  867. SftpFileAttributes attributes = null;
  868. using (var wait = new AutoResetEvent(initialState: false))
  869. {
  870. var request = new SftpLStatRequest(ProtocolVersion,
  871. NextRequestId,
  872. path,
  873. _encoding,
  874. response =>
  875. {
  876. attributes = response.Attributes;
  877. _ = wait.Set();
  878. },
  879. response =>
  880. {
  881. exception = GetSftpException(response);
  882. _ = wait.Set();
  883. });
  884. SendRequest(request);
  885. WaitOnHandle(wait, OperationTimeout);
  886. }
  887. if (exception is not null)
  888. {
  889. throw exception;
  890. }
  891. return attributes;
  892. }
  893. /// <summary>
  894. /// Asynchronously performs SSH_FXP_LSTAT request.
  895. /// </summary>
  896. /// <param name="path">The path.</param>
  897. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  898. /// <returns>
  899. /// A task the represents the asynchronous <c>SSH_FXP_LSTAT</c> request. The value of its
  900. /// <see cref="Task{SftpFileAttributes}.Result"/> contains the file attributes of the specified path.
  901. /// </returns>
  902. public async Task<SftpFileAttributes> RequestLStatAsync(string path, CancellationToken cancellationToken)
  903. {
  904. cancellationToken.ThrowIfCancellationRequested();
  905. var tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);
  906. #if NET || NETSTANDARD2_1_OR_GREATER
  907. await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  908. #else
  909. using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  910. #endif // NET || NETSTANDARD2_1_OR_GREATER
  911. {
  912. SendRequest(new SftpLStatRequest(ProtocolVersion,
  913. NextRequestId,
  914. path,
  915. _encoding,
  916. response => tcs.TrySetResult(response.Attributes),
  917. response => tcs.TrySetException(GetSftpException(response))));
  918. return await tcs.Task.ConfigureAwait(false);
  919. }
  920. }
  921. /// <summary>
  922. /// Performs SSH_FXP_LSTAT request.
  923. /// </summary>
  924. /// <param name="path">The path.</param>
  925. /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginLStat(string, AsyncCallback, object)"/> completes.</param>
  926. /// <param name="state">An object that contains any additional user-defined data.</param>
  927. /// <returns>
  928. /// A <see cref="SFtpStatAsyncResult"/> that represents the asynchronous call.
  929. /// </returns>
  930. public SFtpStatAsyncResult BeginLStat(string path, AsyncCallback callback, object state)
  931. {
  932. var asyncResult = new SFtpStatAsyncResult(callback, state);
  933. var request = new SftpLStatRequest(ProtocolVersion,
  934. NextRequestId,
  935. path,
  936. _encoding,
  937. response =>
  938. {
  939. asyncResult.SetAsCompleted(response.Attributes, completedSynchronously: false);
  940. },
  941. response =>
  942. {
  943. asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false);
  944. });
  945. SendRequest(request);
  946. return asyncResult;
  947. }
  948. /// <summary>
  949. /// Handles the end of an asynchronous SSH_FXP_LSTAT request.
  950. /// </summary>
  951. /// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
  952. /// <returns>
  953. /// The file attributes.
  954. /// </returns>
  955. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  956. public SftpFileAttributes EndLStat(SFtpStatAsyncResult asyncResult)
  957. {
  958. ThrowHelper.ThrowIfNull(asyncResult);
  959. if (asyncResult.EndInvokeCalled)
  960. {
  961. throw new InvalidOperationException("EndLStat has already been called.");
  962. }
  963. if (asyncResult.IsCompleted)
  964. {
  965. return asyncResult.EndInvoke();
  966. }
  967. using (var waitHandle = asyncResult.AsyncWaitHandle)
  968. {
  969. WaitOnHandle(waitHandle, OperationTimeout);
  970. return asyncResult.EndInvoke();
  971. }
  972. }
  973. /// <summary>
  974. /// Performs SSH_FXP_FSTAT request.
  975. /// </summary>
  976. /// <param name="handle">The handle.</param>
  977. /// <param name="nullOnError">If set to <see langword="true"/>, returns <see langword="null"/> instead of throwing an exception.</param>
  978. /// <returns>
  979. /// File attributes.
  980. /// </returns>
  981. public SftpFileAttributes RequestFStat(byte[] handle, bool nullOnError)
  982. {
  983. SshException exception = null;
  984. SftpFileAttributes attributes = null;
  985. using (var wait = new AutoResetEvent(initialState: false))
  986. {
  987. var request = new SftpFStatRequest(ProtocolVersion,
  988. NextRequestId,
  989. handle,
  990. response =>
  991. {
  992. attributes = response.Attributes;
  993. _ = wait.Set();
  994. },
  995. response =>
  996. {
  997. exception = GetSftpException(response);
  998. _ = wait.Set();
  999. });
  1000. SendRequest(request);
  1001. WaitOnHandle(wait, OperationTimeout);
  1002. }
  1003. if (!nullOnError && exception is not null)
  1004. {
  1005. throw exception;
  1006. }
  1007. return attributes;
  1008. }
  1009. /// <summary>
  1010. /// Asynchronously performs a <c>SSH_FXP_FSTAT</c> request.
  1011. /// </summary>
  1012. /// <param name="handle">The handle.</param>
  1013. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  1014. /// <returns>
  1015. /// A task that represents the asynchronous <c>SSH_FXP_FSTAT</c> request. The value of its
  1016. /// <see cref="Task{Task}.Result"/> contains the file attributes of the specified handle.
  1017. /// </returns>
  1018. public async Task<SftpFileAttributes> RequestFStatAsync(byte[] handle, CancellationToken cancellationToken)
  1019. {
  1020. cancellationToken.ThrowIfCancellationRequested();
  1021. var tcs = new TaskCompletionSource<SftpFileAttributes>(TaskCreationOptions.RunContinuationsAsynchronously);
  1022. #if NET || NETSTANDARD2_1_OR_GREATER
  1023. await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  1024. #else
  1025. using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileAttributes>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  1026. #endif // NET || NETSTANDARD2_1_OR_GREATER
  1027. {
  1028. SendRequest(new SftpFStatRequest(ProtocolVersion,
  1029. NextRequestId,
  1030. handle,
  1031. response => tcs.TrySetResult(response.Attributes),
  1032. response => tcs.TrySetException(GetSftpException(response))));
  1033. return await tcs.Task.ConfigureAwait(false);
  1034. }
  1035. }
  1036. /// <summary>
  1037. /// Performs SSH_FXP_SETSTAT request.
  1038. /// </summary>
  1039. /// <param name="path">The path.</param>
  1040. /// <param name="attributes">The attributes.</param>
  1041. public void RequestSetStat(string path, SftpFileAttributes attributes)
  1042. {
  1043. SshException exception = null;
  1044. using (var wait = new AutoResetEvent(initialState: false))
  1045. {
  1046. var request = new SftpSetStatRequest(ProtocolVersion,
  1047. NextRequestId,
  1048. path,
  1049. _encoding,
  1050. attributes,
  1051. response =>
  1052. {
  1053. exception = GetSftpException(response);
  1054. _ = wait.Set();
  1055. });
  1056. SendRequest(request);
  1057. WaitOnHandle(wait, OperationTimeout);
  1058. }
  1059. if (exception is not null)
  1060. {
  1061. throw exception;
  1062. }
  1063. }
  1064. /// <summary>
  1065. /// Performs SSH_FXP_FSETSTAT request.
  1066. /// </summary>
  1067. /// <param name="handle">The handle.</param>
  1068. /// <param name="attributes">The attributes.</param>
  1069. public void RequestFSetStat(byte[] handle, SftpFileAttributes attributes)
  1070. {
  1071. SshException exception = null;
  1072. using (var wait = new AutoResetEvent(initialState: false))
  1073. {
  1074. var request = new SftpFSetStatRequest(ProtocolVersion,
  1075. NextRequestId,
  1076. handle,
  1077. attributes,
  1078. response =>
  1079. {
  1080. exception = GetSftpException(response);
  1081. _ = wait.Set();
  1082. });
  1083. SendRequest(request);
  1084. WaitOnHandle(wait, OperationTimeout);
  1085. }
  1086. if (exception is not null)
  1087. {
  1088. throw exception;
  1089. }
  1090. }
  1091. /// <summary>
  1092. /// Performs SSH_FXP_OPENDIR request.
  1093. /// </summary>
  1094. /// <param name="path">The path.</param>
  1095. /// <param name="nullOnError">If set to <see langword="true"/>, returns <see langword="null"/> instead of throwing an exception.</param>
  1096. /// <returns>File handle.</returns>
  1097. public byte[] RequestOpenDir(string path, bool nullOnError = false)
  1098. {
  1099. SshException exception = null;
  1100. byte[] handle = null;
  1101. using (var wait = new AutoResetEvent(initialState: false))
  1102. {
  1103. var request = new SftpOpenDirRequest(ProtocolVersion,
  1104. NextRequestId,
  1105. path,
  1106. _encoding,
  1107. response =>
  1108. {
  1109. handle = response.Handle;
  1110. _ = wait.Set();
  1111. },
  1112. response =>
  1113. {
  1114. exception = GetSftpException(response);
  1115. _ = wait.Set();
  1116. });
  1117. SendRequest(request);
  1118. WaitOnHandle(wait, OperationTimeout);
  1119. }
  1120. if (!nullOnError && exception is not null)
  1121. {
  1122. throw exception;
  1123. }
  1124. return handle;
  1125. }
  1126. /// <summary>
  1127. /// Asynchronously performs a <c>SSH_FXP_OPENDIR</c> request.
  1128. /// </summary>
  1129. /// <param name="path">The path.</param>
  1130. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  1131. /// <returns>
  1132. /// A task that represents the asynchronous <c>SSH_FXP_OPENDIR</c> request. The value of its
  1133. /// <see cref="Task{Task}.Result"/> contains the handle of the specified path.
  1134. /// </returns>
  1135. public async Task<byte[]> RequestOpenDirAsync(string path, CancellationToken cancellationToken)
  1136. {
  1137. cancellationToken.ThrowIfCancellationRequested();
  1138. var tcs = new TaskCompletionSource<byte[]>(TaskCreationOptions.RunContinuationsAsynchronously);
  1139. #if NET || NETSTANDARD2_1_OR_GREATER
  1140. await using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  1141. #else
  1142. using (cancellationToken.Register(s => ((TaskCompletionSource<byte[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  1143. #endif // NET || NETSTANDARD2_1_OR_GREATER
  1144. {
  1145. SendRequest(new SftpOpenDirRequest(ProtocolVersion,
  1146. NextRequestId,
  1147. path,
  1148. _encoding,
  1149. response => tcs.TrySetResult(response.Handle),
  1150. response => tcs.TrySetException(GetSftpException(response))));
  1151. return await tcs.Task.ConfigureAwait(false);
  1152. }
  1153. }
  1154. /// <summary>
  1155. /// Performs SSH_FXP_READDIR request.
  1156. /// </summary>
  1157. /// <param name="handle">The handle of the directory to read.</param>
  1158. /// <returns>
  1159. /// A <see cref="Dictionary{TKey,TValue}"/> where the <c>key</c> is the name of a file in
  1160. /// the directory and the <c>value</c> is the <see cref="SftpFileAttributes"/> of the file.
  1161. /// </returns>
  1162. public KeyValuePair<string, SftpFileAttributes>[] RequestReadDir(byte[] handle)
  1163. {
  1164. SshException exception = null;
  1165. KeyValuePair<string, SftpFileAttributes>[] result = null;
  1166. using (var wait = new AutoResetEvent(initialState: false))
  1167. {
  1168. var request = new SftpReadDirRequest(ProtocolVersion,
  1169. NextRequestId,
  1170. handle,
  1171. response =>
  1172. {
  1173. result = response.Files;
  1174. _ = wait.Set();
  1175. },
  1176. response =>
  1177. {
  1178. if (response.StatusCode != StatusCodes.Eof)
  1179. {
  1180. exception = GetSftpException(response);
  1181. }
  1182. _ = wait.Set();
  1183. });
  1184. SendRequest(request);
  1185. WaitOnHandle(wait, OperationTimeout);
  1186. }
  1187. if (exception is not null)
  1188. {
  1189. throw exception;
  1190. }
  1191. return result;
  1192. }
  1193. /// <summary>
  1194. /// Performs a <c>SSH_FXP_READDIR</c> request.
  1195. /// </summary>
  1196. /// <param name="handle">The handle of the directory to read.</param>
  1197. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  1198. /// <returns>
  1199. /// A task that represents the asynchronous <c>SSH_FXP_READDIR</c> request. The value of its
  1200. /// <see cref="Task{Task}.Result"/> contains a <see cref="Dictionary{TKey,TValue}"/> where the
  1201. /// <c>key</c> is the name of a file in the directory and the <c>value</c> is the <see cref="SftpFileAttributes"/>
  1202. /// of the file.
  1203. /// </returns>
  1204. public async Task<KeyValuePair<string, SftpFileAttributes>[]> RequestReadDirAsync(byte[] handle, CancellationToken cancellationToken)
  1205. {
  1206. cancellationToken.ThrowIfCancellationRequested();
  1207. var tcs = new TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>(TaskCreationOptions.RunContinuationsAsynchronously);
  1208. #if NET || NETSTANDARD2_1_OR_GREATER
  1209. await using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  1210. #else
  1211. using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  1212. #endif // NET || NETSTANDARD2_1_OR_GREATER
  1213. {
  1214. SendRequest(new SftpReadDirRequest(ProtocolVersion,
  1215. NextRequestId,
  1216. handle,
  1217. response => tcs.TrySetResult(response.Files),
  1218. response =>
  1219. {
  1220. if (response.StatusCode == StatusCodes.Eof)
  1221. {
  1222. _ = tcs.TrySetResult(null);
  1223. }
  1224. else
  1225. {
  1226. _ = tcs.TrySetException(GetSftpException(response));
  1227. }
  1228. }));
  1229. return await tcs.Task.ConfigureAwait(false);
  1230. }
  1231. }
  1232. /// <summary>
  1233. /// Performs SSH_FXP_REMOVE request.
  1234. /// </summary>
  1235. /// <param name="path">The path.</param>
  1236. public void RequestRemove(string path)
  1237. {
  1238. SshException exception = null;
  1239. using (var wait = new AutoResetEvent(initialState: false))
  1240. {
  1241. var request = new SftpRemoveRequest(ProtocolVersion,
  1242. NextRequestId,
  1243. path,
  1244. _encoding,
  1245. response =>
  1246. {
  1247. exception = GetSftpException(response);
  1248. _ = wait.Set();
  1249. });
  1250. SendRequest(request);
  1251. WaitOnHandle(wait, OperationTimeout);
  1252. }
  1253. if (exception is not null)
  1254. {
  1255. throw exception;
  1256. }
  1257. }
  1258. /// <summary>
  1259. /// Asynchronously performs a <c>SSH_FXP_REMOVE</c> request.
  1260. /// </summary>
  1261. /// <param name="path">The path.</param>
  1262. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  1263. /// <returns>
  1264. /// A task that represents the asynchronous <c>SSH_FXP_REMOVE</c> request.
  1265. /// </returns>
  1266. public async Task RequestRemoveAsync(string path, CancellationToken cancellationToken)
  1267. {
  1268. cancellationToken.ThrowIfCancellationRequested();
  1269. var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
  1270. #if NET || NETSTANDARD2_1_OR_GREATER
  1271. await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  1272. #else
  1273. using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  1274. #endif // NET || NETSTANDARD2_1_OR_GREATER
  1275. {
  1276. SendRequest(new SftpRemoveRequest(ProtocolVersion,
  1277. NextRequestId,
  1278. path,
  1279. _encoding,
  1280. response =>
  1281. {
  1282. if (response.StatusCode == StatusCodes.Ok)
  1283. {
  1284. _ = tcs.TrySetResult(true);
  1285. }
  1286. else
  1287. {
  1288. _ = tcs.TrySetException(GetSftpException(response));
  1289. }
  1290. }));
  1291. _ = await tcs.Task.ConfigureAwait(false);
  1292. }
  1293. }
  1294. /// <summary>
  1295. /// Performs SSH_FXP_MKDIR request.
  1296. /// </summary>
  1297. /// <param name="path">The path.</param>
  1298. public void RequestMkDir(string path)
  1299. {
  1300. SshException exception = null;
  1301. using (var wait = new AutoResetEvent(initialState: false))
  1302. {
  1303. var request = new SftpMkDirRequest(ProtocolVersion,
  1304. NextRequestId,
  1305. path,
  1306. _encoding,
  1307. response =>
  1308. {
  1309. exception = GetSftpException(response);
  1310. _ = wait.Set();
  1311. });
  1312. SendRequest(request);
  1313. WaitOnHandle(wait, OperationTimeout);
  1314. }
  1315. if (exception is not null)
  1316. {
  1317. throw exception;
  1318. }
  1319. }
  1320. /// <summary>
  1321. /// Performs SSH_FXP_RMDIR request.
  1322. /// </summary>
  1323. /// <param name="path">The path.</param>
  1324. public void RequestRmDir(string path)
  1325. {
  1326. SshException exception = null;
  1327. using (var wait = new AutoResetEvent(initialState: false))
  1328. {
  1329. var request = new SftpRmDirRequest(ProtocolVersion,
  1330. NextRequestId,
  1331. path,
  1332. _encoding,
  1333. response =>
  1334. {
  1335. exception = GetSftpException(response);
  1336. _ = wait.Set();
  1337. });
  1338. SendRequest(request);
  1339. WaitOnHandle(wait, OperationTimeout);
  1340. }
  1341. if (exception is not null)
  1342. {
  1343. throw exception;
  1344. }
  1345. }
  1346. /// <summary>
  1347. /// Performs SSH_FXP_REALPATH request.
  1348. /// </summary>
  1349. /// <param name="path">The path.</param>
  1350. /// <param name="nullOnError">if set to <see langword="true"/> returns null instead of throwing an exception.</param>
  1351. /// <returns>
  1352. /// The absolute path.
  1353. /// </returns>
  1354. internal KeyValuePair<string, SftpFileAttributes>[] RequestRealPath(string path, bool nullOnError = false)
  1355. {
  1356. SshException exception = null;
  1357. KeyValuePair<string, SftpFileAttributes>[] result = null;
  1358. using (var wait = new AutoResetEvent(initialState: false))
  1359. {
  1360. var request = new SftpRealPathRequest(ProtocolVersion,
  1361. NextRequestId,
  1362. path,
  1363. _encoding,
  1364. response =>
  1365. {
  1366. result = response.Files;
  1367. _ = wait.Set();
  1368. },
  1369. response =>
  1370. {
  1371. exception = GetSftpException(response);
  1372. _ = wait.Set();
  1373. });
  1374. SendRequest(request);
  1375. WaitOnHandle(wait, OperationTimeout);
  1376. }
  1377. if (!nullOnError && exception is not null)
  1378. {
  1379. throw exception;
  1380. }
  1381. return result;
  1382. }
  1383. internal async Task<KeyValuePair<string, SftpFileAttributes>[]> RequestRealPathAsync(string path, bool nullOnError, CancellationToken cancellationToken)
  1384. {
  1385. cancellationToken.ThrowIfCancellationRequested();
  1386. var tcs = new TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>(TaskCreationOptions.RunContinuationsAsynchronously);
  1387. #if NET || NETSTANDARD2_1_OR_GREATER
  1388. await using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  1389. #else
  1390. using (cancellationToken.Register(s => ((TaskCompletionSource<KeyValuePair<string, SftpFileAttributes>[]>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  1391. #endif // NET || NETSTANDARD2_1_OR_GREATER
  1392. {
  1393. SendRequest(new SftpRealPathRequest(ProtocolVersion,
  1394. NextRequestId,
  1395. path,
  1396. _encoding,
  1397. response => tcs.TrySetResult(response.Files),
  1398. response =>
  1399. {
  1400. if (nullOnError)
  1401. {
  1402. _ = tcs.TrySetResult(null);
  1403. }
  1404. else
  1405. {
  1406. _ = tcs.TrySetException(GetSftpException(response));
  1407. }
  1408. }));
  1409. return await tcs.Task.ConfigureAwait(false);
  1410. }
  1411. }
  1412. /// <summary>
  1413. /// Performs SSH_FXP_REALPATH request.
  1414. /// </summary>
  1415. /// <param name="path">The path.</param>
  1416. /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginRealPath(string, AsyncCallback, object)"/> completes.</param>
  1417. /// <param name="state">An object that contains any additional user-defined data.</param>
  1418. /// <returns>
  1419. /// A <see cref="SftpRealPathAsyncResult"/> that represents the asynchronous call.
  1420. /// </returns>
  1421. public SftpRealPathAsyncResult BeginRealPath(string path, AsyncCallback callback, object state)
  1422. {
  1423. var asyncResult = new SftpRealPathAsyncResult(callback, state);
  1424. var request = new SftpRealPathRequest(ProtocolVersion,
  1425. NextRequestId,
  1426. path,
  1427. _encoding,
  1428. response => asyncResult.SetAsCompleted(response.Files[0].Key, completedSynchronously: false),
  1429. response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false));
  1430. SendRequest(request);
  1431. return asyncResult;
  1432. }
  1433. /// <summary>
  1434. /// Handles the end of an asynchronous SSH_FXP_REALPATH request.
  1435. /// </summary>
  1436. /// <param name="asyncResult">An <see cref="SftpRealPathAsyncResult"/> that represents an asynchronous call.</param>
  1437. /// <returns>
  1438. /// The absolute path.
  1439. /// </returns>
  1440. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  1441. public string EndRealPath(SftpRealPathAsyncResult asyncResult)
  1442. {
  1443. ThrowHelper.ThrowIfNull(asyncResult);
  1444. if (asyncResult.EndInvokeCalled)
  1445. {
  1446. throw new InvalidOperationException("EndRealPath has already been called.");
  1447. }
  1448. if (asyncResult.IsCompleted)
  1449. {
  1450. return asyncResult.EndInvoke();
  1451. }
  1452. using (var waitHandle = asyncResult.AsyncWaitHandle)
  1453. {
  1454. WaitOnHandle(waitHandle, OperationTimeout);
  1455. return asyncResult.EndInvoke();
  1456. }
  1457. }
  1458. /// <summary>
  1459. /// Performs SSH_FXP_STAT request.
  1460. /// </summary>
  1461. /// <param name="path">The path.</param>
  1462. /// <param name="nullOnError">if set to <see langword="true"/> returns null instead of throwing an exception.</param>
  1463. /// <returns>
  1464. /// File attributes.
  1465. /// </returns>
  1466. public SftpFileAttributes RequestStat(string path, bool nullOnError = false)
  1467. {
  1468. SshException exception = null;
  1469. SftpFileAttributes attributes = null;
  1470. using (var wait = new AutoResetEvent(initialState: false))
  1471. {
  1472. var request = new SftpStatRequest(ProtocolVersion,
  1473. NextRequestId,
  1474. path,
  1475. _encoding,
  1476. response =>
  1477. {
  1478. attributes = response.Attributes;
  1479. _ = wait.Set();
  1480. },
  1481. response =>
  1482. {
  1483. exception = GetSftpException(response);
  1484. _ = wait.Set();
  1485. });
  1486. SendRequest(request);
  1487. WaitOnHandle(wait, OperationTimeout);
  1488. }
  1489. if (!nullOnError && exception is not null)
  1490. {
  1491. throw exception;
  1492. }
  1493. return attributes;
  1494. }
  1495. /// <summary>
  1496. /// Performs SSH_FXP_STAT request.
  1497. /// </summary>
  1498. /// <param name="path">The path.</param>
  1499. /// <param name="callback">The <see cref="AsyncCallback"/> delegate that is executed when <see cref="BeginStat(string, AsyncCallback, object)"/> completes.</param>
  1500. /// <param name="state">An object that contains any additional user-defined data.</param>
  1501. /// <returns>
  1502. /// A <see cref="SFtpStatAsyncResult"/> that represents the asynchronous call.
  1503. /// </returns>
  1504. public SFtpStatAsyncResult BeginStat(string path, AsyncCallback callback, object state)
  1505. {
  1506. var asyncResult = new SFtpStatAsyncResult(callback, state);
  1507. var request = new SftpStatRequest(ProtocolVersion,
  1508. NextRequestId,
  1509. path,
  1510. _encoding,
  1511. response => asyncResult.SetAsCompleted(response.Attributes, completedSynchronously: false),
  1512. response => asyncResult.SetAsCompleted(GetSftpException(response), completedSynchronously: false));
  1513. SendRequest(request);
  1514. return asyncResult;
  1515. }
  1516. /// <summary>
  1517. /// Handles the end of an asynchronous stat.
  1518. /// </summary>
  1519. /// <param name="asyncResult">An <see cref="SFtpStatAsyncResult"/> that represents an asynchronous call.</param>
  1520. /// <returns>
  1521. /// The file attributes.
  1522. /// </returns>
  1523. /// <exception cref="ArgumentNullException"><paramref name="asyncResult"/> is <see langword="null"/>.</exception>
  1524. public SftpFileAttributes EndStat(SFtpStatAsyncResult asyncResult)
  1525. {
  1526. ThrowHelper.ThrowIfNull(asyncResult);
  1527. if (asyncResult.EndInvokeCalled)
  1528. {
  1529. throw new InvalidOperationException("EndStat has already been called.");
  1530. }
  1531. if (asyncResult.IsCompleted)
  1532. {
  1533. return asyncResult.EndInvoke();
  1534. }
  1535. using (var waitHandle = asyncResult.AsyncWaitHandle)
  1536. {
  1537. WaitOnHandle(waitHandle, OperationTimeout);
  1538. return asyncResult.EndInvoke();
  1539. }
  1540. }
  1541. /// <summary>
  1542. /// Performs SSH_FXP_RENAME request.
  1543. /// </summary>
  1544. /// <param name="oldPath">The old path.</param>
  1545. /// <param name="newPath">The new path.</param>
  1546. public void RequestRename(string oldPath, string newPath)
  1547. {
  1548. if (ProtocolVersion < 2)
  1549. {
  1550. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_RENAME operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1551. }
  1552. SshException exception = null;
  1553. using (var wait = new AutoResetEvent(initialState: false))
  1554. {
  1555. var request = new SftpRenameRequest(ProtocolVersion,
  1556. NextRequestId,
  1557. oldPath,
  1558. newPath,
  1559. _encoding,
  1560. response =>
  1561. {
  1562. exception = GetSftpException(response);
  1563. _ = wait.Set();
  1564. });
  1565. SendRequest(request);
  1566. WaitOnHandle(wait, OperationTimeout);
  1567. }
  1568. if (exception is not null)
  1569. {
  1570. throw exception;
  1571. }
  1572. }
  1573. /// <summary>
  1574. /// Asynchronously performs a <c>SSH_FXP_RENAME</c> request.
  1575. /// </summary>
  1576. /// <param name="oldPath">The old path.</param>
  1577. /// <param name="newPath">The new path.</param>
  1578. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  1579. /// <returns>
  1580. /// A task that represents the asynchronous <c>SSH_FXP_RENAME</c> request.
  1581. /// </returns>
  1582. public async Task RequestRenameAsync(string oldPath, string newPath, CancellationToken cancellationToken)
  1583. {
  1584. cancellationToken.ThrowIfCancellationRequested();
  1585. var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
  1586. #if NET || NETSTANDARD2_1_OR_GREATER
  1587. await using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  1588. #else
  1589. using (cancellationToken.Register(s => ((TaskCompletionSource<bool>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  1590. #endif // NET || NETSTANDARD2_1_OR_GREATER
  1591. {
  1592. SendRequest(new SftpRenameRequest(ProtocolVersion,
  1593. NextRequestId,
  1594. oldPath,
  1595. newPath,
  1596. _encoding,
  1597. response =>
  1598. {
  1599. if (response.StatusCode == StatusCodes.Ok)
  1600. {
  1601. _ = tcs.TrySetResult(true);
  1602. }
  1603. else
  1604. {
  1605. _ = tcs.TrySetException(GetSftpException(response));
  1606. }
  1607. }));
  1608. _ = await tcs.Task.ConfigureAwait(false);
  1609. }
  1610. }
  1611. /// <summary>
  1612. /// Performs SSH_FXP_READLINK request.
  1613. /// </summary>
  1614. /// <param name="path">The path.</param>
  1615. /// <param name="nullOnError">if set to <see langword="true"/> returns <see langword="null"/> instead of throwing an exception.</param>
  1616. /// <returns>
  1617. /// An array of <see cref="KeyValuePair{TKey,TValue}"/> where the <c>key</c> is the name of
  1618. /// a file and the <c>value</c> is the <see cref="SftpFileAttributes"/> of the file.
  1619. /// </returns>
  1620. internal KeyValuePair<string, SftpFileAttributes>[] RequestReadLink(string path, bool nullOnError = false)
  1621. {
  1622. if (ProtocolVersion < 3)
  1623. {
  1624. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_READLINK operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1625. }
  1626. SshException exception = null;
  1627. KeyValuePair<string, SftpFileAttributes>[] result = null;
  1628. using (var wait = new AutoResetEvent(initialState: false))
  1629. {
  1630. var request = new SftpReadLinkRequest(ProtocolVersion,
  1631. NextRequestId,
  1632. path,
  1633. _encoding,
  1634. response =>
  1635. {
  1636. result = response.Files;
  1637. _ = wait.Set();
  1638. },
  1639. response =>
  1640. {
  1641. exception = GetSftpException(response);
  1642. _ = wait.Set();
  1643. });
  1644. SendRequest(request);
  1645. WaitOnHandle(wait, OperationTimeout);
  1646. }
  1647. if (!nullOnError && exception is not null)
  1648. {
  1649. throw exception;
  1650. }
  1651. return result;
  1652. }
  1653. /// <summary>
  1654. /// Performs SSH_FXP_SYMLINK request.
  1655. /// </summary>
  1656. /// <param name="linkpath">The linkpath.</param>
  1657. /// <param name="targetpath">The targetpath.</param>
  1658. public void RequestSymLink(string linkpath, string targetpath)
  1659. {
  1660. if (ProtocolVersion < 3)
  1661. {
  1662. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_SYMLINK operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1663. }
  1664. SshException exception = null;
  1665. using (var wait = new AutoResetEvent(initialState: false))
  1666. {
  1667. var request = new SftpSymLinkRequest(ProtocolVersion,
  1668. NextRequestId,
  1669. linkpath,
  1670. targetpath,
  1671. _encoding,
  1672. response =>
  1673. {
  1674. exception = GetSftpException(response);
  1675. _ = wait.Set();
  1676. });
  1677. SendRequest(request);
  1678. WaitOnHandle(wait, OperationTimeout);
  1679. }
  1680. if (exception is not null)
  1681. {
  1682. throw exception;
  1683. }
  1684. }
  1685. /// <summary>
  1686. /// Performs posix-rename@openssh.com extended request.
  1687. /// </summary>
  1688. /// <param name="oldPath">The old path.</param>
  1689. /// <param name="newPath">The new path.</param>
  1690. public void RequestPosixRename(string oldPath, string newPath)
  1691. {
  1692. if (ProtocolVersion < 3)
  1693. {
  1694. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1695. }
  1696. SshException exception = null;
  1697. using (var wait = new AutoResetEvent(initialState: false))
  1698. {
  1699. var request = new PosixRenameRequest(ProtocolVersion,
  1700. NextRequestId,
  1701. oldPath,
  1702. newPath,
  1703. _encoding,
  1704. response =>
  1705. {
  1706. exception = GetSftpException(response);
  1707. _ = wait.Set();
  1708. });
  1709. if (!_supportedExtensions.ContainsKey(request.Name))
  1710. {
  1711. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
  1712. }
  1713. SendRequest(request);
  1714. WaitOnHandle(wait, OperationTimeout);
  1715. }
  1716. if (exception is not null)
  1717. {
  1718. throw exception;
  1719. }
  1720. }
  1721. /// <summary>
  1722. /// Performs statvfs@openssh.com extended request.
  1723. /// </summary>
  1724. /// <param name="path">The path.</param>
  1725. /// <param name="nullOnError">if set to <see langword="true"/> [null on error].</param>
  1726. /// <returns>
  1727. /// A <see cref="SftpFileSystemInformation"/> for the specified path.
  1728. /// </returns>
  1729. public SftpFileSystemInformation RequestStatVfs(string path, bool nullOnError = false)
  1730. {
  1731. if (ProtocolVersion < 3)
  1732. {
  1733. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1734. }
  1735. SshException exception = null;
  1736. SftpFileSystemInformation information = null;
  1737. using (var wait = new AutoResetEvent(initialState: false))
  1738. {
  1739. var request = new StatVfsRequest(ProtocolVersion,
  1740. NextRequestId,
  1741. path,
  1742. _encoding,
  1743. response =>
  1744. {
  1745. information = response.GetReply<StatVfsReplyInfo>().Information;
  1746. _ = wait.Set();
  1747. },
  1748. response =>
  1749. {
  1750. exception = GetSftpException(response);
  1751. _ = wait.Set();
  1752. });
  1753. if (!_supportedExtensions.ContainsKey(request.Name))
  1754. {
  1755. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
  1756. }
  1757. SendRequest(request);
  1758. WaitOnHandle(wait, OperationTimeout);
  1759. }
  1760. if (!nullOnError && exception is not null)
  1761. {
  1762. throw exception;
  1763. }
  1764. return information;
  1765. }
  1766. /// <summary>
  1767. /// Asynchronously performs a <c>statvfs@openssh.com</c> extended request.
  1768. /// </summary>
  1769. /// <param name="path">The path.</param>
  1770. /// <param name="cancellationToken">The token to monitor for cancellation requests.</param>
  1771. /// <returns>
  1772. /// A task that represents the <c>statvfs@openssh.com</c> extended request. The value of its
  1773. /// <see cref="Task{Task}.Result"/> contains the file system information for the specified
  1774. /// path.
  1775. /// </returns>
  1776. public async Task<SftpFileSystemInformation> RequestStatVfsAsync(string path, CancellationToken cancellationToken)
  1777. {
  1778. if (ProtocolVersion < 3)
  1779. {
  1780. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1781. }
  1782. cancellationToken.ThrowIfCancellationRequested();
  1783. var tcs = new TaskCompletionSource<SftpFileSystemInformation>(TaskCreationOptions.RunContinuationsAsynchronously);
  1784. #if NET || NETSTANDARD2_1_OR_GREATER
  1785. await using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileSystemInformation>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false).ConfigureAwait(continueOnCapturedContext: false))
  1786. #else
  1787. using (cancellationToken.Register(s => ((TaskCompletionSource<SftpFileSystemInformation>)s).TrySetCanceled(cancellationToken), tcs, useSynchronizationContext: false))
  1788. #endif // NET || NETSTANDARD2_1_OR_GREATER
  1789. {
  1790. SendRequest(new StatVfsRequest(ProtocolVersion,
  1791. NextRequestId,
  1792. path,
  1793. _encoding,
  1794. response => tcs.TrySetResult(response.GetReply<StatVfsReplyInfo>().Information),
  1795. response => tcs.TrySetException(GetSftpException(response))));
  1796. return await tcs.Task.ConfigureAwait(false);
  1797. }
  1798. }
  1799. /// <summary>
  1800. /// Performs fstatvfs@openssh.com extended request.
  1801. /// </summary>
  1802. /// <param name="handle">The file handle.</param>
  1803. /// <param name="nullOnError">if set to <see langword="true"/> [null on error].</param>
  1804. /// <returns>
  1805. /// A <see cref="SftpFileSystemInformation"/> for the specified path.
  1806. /// </returns>
  1807. /// <exception cref="NotSupportedException">This operation is not supported for the current SFTP protocol version.</exception>
  1808. internal SftpFileSystemInformation RequestFStatVfs(byte[] handle, bool nullOnError = false)
  1809. {
  1810. if (ProtocolVersion < 3)
  1811. {
  1812. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1813. }
  1814. SshException exception = null;
  1815. SftpFileSystemInformation information = null;
  1816. using (var wait = new AutoResetEvent(initialState: false))
  1817. {
  1818. var request = new FStatVfsRequest(ProtocolVersion,
  1819. NextRequestId,
  1820. handle,
  1821. response =>
  1822. {
  1823. information = response.GetReply<StatVfsReplyInfo>().Information;
  1824. _ = wait.Set();
  1825. },
  1826. response =>
  1827. {
  1828. exception = GetSftpException(response);
  1829. _ = wait.Set();
  1830. });
  1831. if (!_supportedExtensions.ContainsKey(request.Name))
  1832. {
  1833. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
  1834. }
  1835. SendRequest(request);
  1836. WaitOnHandle(wait, OperationTimeout);
  1837. }
  1838. if (!nullOnError && exception is not null)
  1839. {
  1840. throw exception;
  1841. }
  1842. return information;
  1843. }
  1844. /// <summary>
  1845. /// Performs hardlink@openssh.com extended request.
  1846. /// </summary>
  1847. /// <param name="oldPath">The old path.</param>
  1848. /// <param name="newPath">The new path.</param>
  1849. internal void HardLink(string oldPath, string newPath)
  1850. {
  1851. if (ProtocolVersion < 3)
  1852. {
  1853. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "SSH_FXP_EXTENDED operation is not supported in {0} version that server operates in.", ProtocolVersion));
  1854. }
  1855. SshException exception = null;
  1856. using (var wait = new AutoResetEvent(initialState: false))
  1857. {
  1858. var request = new HardLinkRequest(ProtocolVersion,
  1859. NextRequestId,
  1860. oldPath,
  1861. newPath,
  1862. response =>
  1863. {
  1864. exception = GetSftpException(response);
  1865. _ = wait.Set();
  1866. });
  1867. if (!_supportedExtensions.ContainsKey(request.Name))
  1868. {
  1869. throw new NotSupportedException(string.Format(CultureInfo.CurrentCulture, "Extension method {0} currently not supported by the server.", request.Name));
  1870. }
  1871. SendRequest(request);
  1872. WaitOnHandle(wait, OperationTimeout);
  1873. }
  1874. if (exception is not null)
  1875. {
  1876. throw exception;
  1877. }
  1878. }
  1879. /// <summary>
  1880. /// Calculates the optimal size of the buffer to read data from the channel.
  1881. /// </summary>
  1882. /// <param name="bufferSize">The buffer size configured on the client.</param>
  1883. /// <returns>
  1884. /// The optimal size of the buffer to read data from the channel.
  1885. /// </returns>
  1886. public uint CalculateOptimalReadLength(uint bufferSize)
  1887. {
  1888. // a SSH_FXP_DATA message has 13 bytes of protocol fields:
  1889. // bytes 1 to 4: packet length
  1890. // byte 5: message type
  1891. // bytes 6 to 9: response id
  1892. // bytes 10 to 13: length of payload
  1893. //
  1894. // WinSCP uses a payload length of 32755 bytes
  1895. //
  1896. // most ssh servers limit the size of the payload of a SSH_MSG_CHANNEL_DATA
  1897. // response to 16 KB; if we requested 16 KB of data, then the SSH_FXP_DATA
  1898. // payload of the SSH_MSG_CHANNEL_DATA message would be too big (16 KB + 13 bytes), and
  1899. // as a result, the ssh server would split this into two responses:
  1900. // one containing 16384 bytes (13 bytes header, and 16371 bytes file data)
  1901. // and one with the remaining 13 bytes of file data
  1902. const uint lengthOfNonDataProtocolFields = 13u;
  1903. var maximumPacketSize = Channel.LocalPacketSize;
  1904. return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields;
  1905. }
  1906. /// <summary>
  1907. /// Calculates the optimal size of the buffer to write data on the channel.
  1908. /// </summary>
  1909. /// <param name="bufferSize">The buffer size configured on the client.</param>
  1910. /// <param name="handle">The file handle.</param>
  1911. /// <returns>
  1912. /// The optimal size of the buffer to write data on the channel.
  1913. /// </returns>
  1914. /// <remarks>
  1915. /// Currently, we do not take the remote window size into account.
  1916. /// </remarks>
  1917. public uint CalculateOptimalWriteLength(uint bufferSize, byte[] handle)
  1918. {
  1919. // 1-4: package length of SSH_FXP_WRITE message
  1920. // 5: message type
  1921. // 6-9: request id
  1922. // 10-13: handle length
  1923. // <handle>
  1924. // 14-21: offset
  1925. // 22-25: data length
  1926. /*
  1927. * Putty uses data length of 4096 bytes
  1928. * WinSCP uses data length of 32739 bytes (total 32768 bytes; 32739 + 25 + 4 bytes for handle)
  1929. */
  1930. var lengthOfNonDataProtocolFields = 25u + (uint)handle.Length;
  1931. var maximumPacketSize = Channel.RemotePacketSize;
  1932. return Math.Min(bufferSize, maximumPacketSize) - lengthOfNonDataProtocolFields;
  1933. }
  1934. private static SshException GetSftpException(SftpStatusResponse response)
  1935. {
  1936. #pragma warning disable IDE0010 // Add missing cases
  1937. switch (response.StatusCode)
  1938. {
  1939. case StatusCodes.Ok:
  1940. return null;
  1941. case StatusCodes.PermissionDenied:
  1942. return new SftpPermissionDeniedException(response.ErrorMessage);
  1943. case StatusCodes.NoSuchFile:
  1944. return new SftpPathNotFoundException(response.ErrorMessage);
  1945. default:
  1946. return new SshException(response.ErrorMessage);
  1947. }
  1948. #pragma warning restore IDE0010 // Add missing cases
  1949. }
  1950. private void HandleResponse(SftpResponse response)
  1951. {
  1952. SftpRequest request;
  1953. lock (_requests)
  1954. {
  1955. _ = _requests.TryGetValue(response.ResponseId, out request);
  1956. if (request is not null)
  1957. {
  1958. _ = _requests.Remove(response.ResponseId);
  1959. }
  1960. }
  1961. if (request is null)
  1962. {
  1963. throw new InvalidOperationException("Invalid response.");
  1964. }
  1965. request.Complete(response);
  1966. }
  1967. }
  1968. }