Skip to content

Commit 19b1153

Browse files
Merge pull request #2 from richardschneider/httpclient
Asynchronous I/O
2 parents 89487c7 + 5627837 commit 19b1153

File tree

14 files changed

+262
-63
lines changed

14 files changed

+262
-63
lines changed

appveyor.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ nuget:
3030

3131
before_build:
3232
- nuget restore
33-
- ps: gitversion /output buildserver /updateAssemblyInfo >gitversion.log
33+
- ps: gitversion /output buildserver /updateAssemblyInfo
3434

3535
platform: Any CPU
3636
configuration: Release

src/Add.cs

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
using System.Linq;
44
using System.Text;
55
using System.IO;
6+
using System.Threading.Tasks;
7+
using System.Net.Http;
8+
using System.Net.Http.Headers;
9+
using Newtonsoft.Json.Linq;
610

711
namespace Ipfs.Api
812
{
@@ -12,16 +16,16 @@ public partial class IpfsClient
1216
/// Add a file to the interplanetary file system.
1317
/// </summary>
1418
/// <param name="path"></param>
15-
public MerkleNode AddFile(string path)
19+
public Task<MerkleNode> AddFileAsync(string path)
1620
{
17-
throw new NotImplementedException();
21+
return AddAsync(new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read));
1822
}
1923

2024
/// <summary>
2125
/// Add a directory and its files to the interplanetary file system.
2226
/// </summary>
2327
/// <param name="path"></param>
24-
public MerkleNode AddDirectory(string path, bool recursive = true)
28+
public Task<MerkleNode> AddDirectoryAsync(string path, bool recursive = true)
2529
{
2630
throw new NotImplementedException();
2731
}
@@ -30,18 +34,46 @@ public MerkleNode AddDirectory(string path, bool recursive = true)
3034
/// Add some text to the interplanetary file system.
3135
/// </summary>
3236
/// <param name="text"></param>
33-
public MerkleNode AddText(string text)
37+
public Task<MerkleNode> AddTextAsync(string text)
3438
{
35-
throw new NotImplementedException();
39+
return AddAsync(new MemoryStream(Encoding.UTF8.GetBytes(text)));
3640
}
3741

3842
/// <summary>
3943
/// Add a <see cref="Stream"/> to interplanetary file system.
4044
/// </summary>
41-
/// <param name="s"></param>
42-
public MerkleNode Add(Stream s)
45+
/// <param name="stream"></param>
46+
public async Task<MerkleNode> AddAsync(Stream stream)
4347
{
44-
throw new NotImplementedException();
48+
var content = new MultipartFormDataContent();
49+
var streamContent = new StreamContent(stream);
50+
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
51+
content.Add(streamContent, "file");
52+
53+
try
54+
{
55+
var url = BuildCommand("add");
56+
if (log.IsDebugEnabled)
57+
log.Debug("POST " + url.ToString());
58+
using (var response = await Api().PostAsync(url, content))
59+
{
60+
await ThrowOnError(response);
61+
var json = await response.Content.ReadAsStringAsync();
62+
if (log.IsDebugEnabled)
63+
log.Debug("RSP " + json);
64+
var r = JObject.Parse(json);
65+
66+
return new MerkleNode((string)r["Hash"]);
67+
}
68+
}
69+
catch (IpfsException)
70+
{
71+
throw;
72+
}
73+
catch (Exception e)
74+
{
75+
throw new IpfsException(e);
76+
}
4577
}
4678
}
4779
}

src/Id.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Text;
66
using System.IO;
77
using System.Net;
8+
using System.Threading.Tasks;
89

910
namespace Ipfs.Api
1011
{
@@ -19,9 +20,9 @@ public partial class IpfsClient
1920
/// </param>
2021
/// <returns>
2122
/// </returns>
22-
public PeerNode Id(string peer = null)
23+
public Task<PeerNode> Id(string peer = null)
2324
{
24-
return DoCommand<PeerNode>("id", peer);
25+
return DoCommandAsync<PeerNode>("id", peer);
2526
}
2627
}
2728
}

src/IpfsApi.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@
5858
<Reference Include="System" />
5959
<Reference Include="System.Core" />
6060
<Reference Include="Microsoft.CSharp" />
61-
<Reference Include="System.Web" />
61+
<Reference Include="System.Net.Http" />
6262
</ItemGroup>
6363
<ItemGroup>
6464
<Compile Include="Add.cs" />

src/IpfsClient.cs

Lines changed: 149 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
using System.Collections.Generic;
44
using System.Linq;
55
using System.Text;
6-
using System.Net;
76
using System.Reflection;
8-
using System.Web;
97
using Newtonsoft.Json;
108
using System.IO;
9+
using System.Net.Http;
10+
using System.Threading.Tasks;
11+
using System.Net;
1112

1213
namespace Ipfs.Api
1314
{
@@ -19,9 +20,15 @@ namespace Ipfs.Api
1920
/// </remarks>
2021
/// <seealso href="https://ipfs.io/docs/api/">IPFS API</seealso>
2122
/// <seealso href="https://ipfs.io/docs/commands/">IPFS commands</seealso>
23+
/// <remarks>
24+
/// <b>IpfsClient</b> is thread safe, only one instance is required
25+
/// by the application.
26+
/// </remarks>
2227
public partial class IpfsClient
2328
{
2429
static ILog log = LogManager.GetCurrentClassLogger();
30+
static object safe = new object();
31+
static HttpClient api = null;
2532

2633
/// <summary>
2734
/// The default URL to the IPFS API server. The default is "http://localhost:5001".
@@ -83,7 +90,7 @@ Uri BuildCommand(string command, string arg = null, params string[] options)
8390
if (arg != null)
8491
{
8592
q.Append("&arg=");
86-
q.Append(HttpUtility.UrlEncode(arg));
93+
q.Append(WebUtility.UrlEncode(arg));
8794
}
8895

8996
foreach (var option in options)
@@ -98,7 +105,7 @@ Uri BuildCommand(string command, string arg = null, params string[] options)
98105
{
99106
q.Append(option.Substring(0, i));
100107
q.Append('=');
101-
q.Append(HttpUtility.UrlEncode(option.Substring(i + 1)));
108+
q.Append(WebUtility.UrlEncode(option.Substring(i + 1)));
102109
}
103110
}
104111

@@ -112,13 +119,28 @@ Uri BuildCommand(string command, string arg = null, params string[] options)
112119
return new Uri(ApiUri, url);
113120
}
114121

115-
WebClient Api()
122+
/// <summary>
123+
/// Get the IPFS API.
124+
/// </summary>
125+
/// <returns>
126+
/// A <see cref="HttpClient"/>.
127+
/// </returns>
128+
/// <remarks>
129+
/// Only one client is needed. Its thread safe.
130+
/// </remarks>
131+
HttpClient Api()
116132
{
117-
var api = new WebClient
133+
if (api == null)
118134
{
119-
Encoding = Encoding.UTF8
120-
};
121-
api.Headers["User-Agent"] = UserAgent;
135+
lock (safe)
136+
{
137+
if (api == null)
138+
{
139+
api = new HttpClient();
140+
api.DefaultRequestHeaders.Add("User-Agent", UserAgent);
141+
}
142+
}
143+
}
122144
return api;
123145
}
124146

@@ -138,17 +160,25 @@ WebClient Api()
138160
/// <returns>
139161
/// A string representation of the command's result.
140162
/// </returns>
141-
public string DoCommand(string command, string arg = null, params string[] options)
163+
public async Task<string> DoCommandAsync(string command, string arg = null, params string[] options)
142164
{
143165
try
144166
{
145167
var url = BuildCommand(command, arg, options);
146168
if (log.IsDebugEnabled)
147169
log.Debug("GET " + url.ToString());
148-
var s = Api().DownloadString(url);
149-
if (log.IsDebugEnabled)
150-
log.Debug("RSP " + s);
151-
return s;
170+
using (var response = await Api().GetAsync(url))
171+
{
172+
await ThrowOnError(response);
173+
var body = await response.Content.ReadAsStringAsync();
174+
if (log.IsDebugEnabled)
175+
log.Debug("RSP " + body);
176+
return body;
177+
}
178+
}
179+
catch (IpfsException)
180+
{
181+
throw;
152182
}
153183
catch (Exception e)
154184
{
@@ -180,9 +210,9 @@ public string DoCommand(string command, string arg = null, params string[] optio
180210
/// The command's response is converted to <typeparamref name="T"/> using
181211
/// <c>JsonConvert</c>.
182212
/// </remarks>
183-
public T DoCommand<T>(string command, string arg = null, params string[] options)
213+
public async Task<T> DoCommandAsync<T>(string command, string arg = null, params string[] options)
184214
{
185-
var json = DoCommand(command, arg, options);
215+
var json = await DoCommandAsync(command, arg, options);
186216
return JsonConvert.DeserializeObject<T>(json);
187217
}
188218

@@ -203,20 +233,120 @@ public T DoCommand<T>(string command, string arg = null, params string[] options
203233
/// <returns>
204234
/// A <see cref="Stream"/> containing the command's result.
205235
/// </returns>
206-
public Stream Download(string command, string arg = null, params string[] options)
236+
public async Task<Stream> DownloadAsync(string command, string arg = null, params string[] options)
207237
{
208238
try
209239
{
210240
var url = BuildCommand(command, arg, options);
211241
if (log.IsDebugEnabled)
212242
log.Debug("GET " + url.ToString());
213-
return Api().OpenRead(url);
243+
var response = await Api().GetAsync(url);
244+
await ThrowOnError(response);
245+
return await response.Content.ReadAsStreamAsync();
246+
}
247+
catch (IpfsException)
248+
{
249+
throw;
214250
}
215251
catch (Exception e)
216252
{
217253
throw new IpfsException(e);
218254
}
219255
}
220-
256+
257+
/// <summary>
258+
/// Perform an <see href="https://ipfs.io/docs/api/">IPFS API command</see> returning a string.
259+
/// </summary>
260+
/// <param name="command">
261+
/// The <see href="https://ipfs.io/docs/api/">IPFS API command</see>, such as
262+
/// <see href="https://ipfs.io/docs/api/#apiv0filels">"file/ls"</see>.
263+
/// </param>
264+
/// <param name="arg">
265+
/// The optional argument to the command.
266+
/// </param>
267+
/// <param name="options">
268+
/// The optional flags to the command.
269+
/// </param>
270+
/// <returns>
271+
/// A string representation of the command's result.
272+
/// </returns>
273+
public string DoCommand(string command, string arg = null, params string[] options)
274+
{
275+
return DoCommandAsync(command, arg, options).Result;
276+
}
277+
278+
/// <summary>
279+
/// Perform an <see href="https://ipfs.io/docs/api/">IPFS API command</see> returning
280+
/// a specific <see cref="Type"/>.
281+
/// </summary>
282+
/// <typeparam name="T">
283+
/// The <see cref="Type"/> of object to return.
284+
/// </typeparam>
285+
/// <param name="command">
286+
/// The <see href="https://ipfs.io/docs/api/">IPFS API command</see>, such as
287+
/// <see href="https://ipfs.io/docs/api/#apiv0filels">"file/ls"</see>.
288+
/// </param>
289+
/// <param name="arg">
290+
/// The optional argument to the command.
291+
/// </param>
292+
/// <param name="options">
293+
/// The optional flags to the command.
294+
/// </param>
295+
/// <returns>
296+
/// A <typeparamref name="T"/>.
297+
/// </returns>
298+
/// <remarks>
299+
/// The command's response is converted to <typeparamref name="T"/> using
300+
/// <c>JsonConvert</c>.
301+
/// </remarks>
302+
public T DoCommand<T>(string command, string arg = null, params string[] options)
303+
{
304+
return DoCommandAsync<T>(command, arg, options).Result;
305+
}
306+
307+
/// <summary>
308+
/// Perform an <see href="https://ipfs.io/docs/api/">IPFS API command</see> returning a
309+
/// <see cref="Stream"/>.
310+
/// </summary>
311+
/// <param name="command">
312+
/// The <see href="https://ipfs.io/docs/api/">IPFS API command</see>, such as
313+
/// <see href="https://ipfs.io/docs/api/#apiv0filels">"file/ls"</see>.
314+
/// </param>
315+
/// <param name="arg">
316+
/// The optional argument to the command.
317+
/// </param>
318+
/// <param name="options">
319+
/// The optional flags to the command.
320+
/// </param>
321+
/// <returns>
322+
/// A <see cref="Stream"/> containing the command's result.
323+
/// </returns>
324+
public Stream Download(string command, string arg = null, params string[] options)
325+
{
326+
return DownloadAsync(command, arg, options).Result;
327+
}
328+
329+
/// <summary>
330+
///
331+
/// </summary>
332+
/// <param name="response"></param>
333+
/// <returns></returns>
334+
/// <remarks>
335+
/// The API server returns an JSON error in the form <c>{ "Message": "...", "Code": ... }</c>.
336+
/// </remarks>
337+
async Task<bool> ThrowOnError(HttpResponseMessage response)
338+
{
339+
if (response.IsSuccessStatusCode)
340+
return true; ;
341+
if (response.StatusCode == HttpStatusCode.NotFound)
342+
throw new IpfsException("Invalid command");
343+
344+
var body = await response.Content.ReadAsStringAsync();
345+
if (log.IsDebugEnabled)
346+
log.Debug("ERR " + body);
347+
var message = (string)JsonConvert.DeserializeObject<dynamic>(body).Message;
348+
throw new IpfsException(message);
349+
}
350+
221351
}
222352
}

0 commit comments

Comments
 (0)