SftpSession.cs 100 KB

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