Skip to content

Commit 4e84088

Browse files
committed
Pin API: records-based parsing; AddAsync overload with IProgress; ListAsync options overload; tests for progress and streaming
1 parent 6f247ac commit 4e84088

File tree

8 files changed

+297
-45
lines changed

8 files changed

+297
-45
lines changed

src/CoreApi/PinApi.cs

Lines changed: 148 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
1-
using Google.Protobuf;
2-
using Ipfs.CoreApi;
3-
using Newtonsoft.Json.Linq;
1+
using Ipfs.CoreApi;
42
using System.Collections.Generic;
53
using System.Linq;
64
using System.Threading;
75
using System.Threading.Tasks;
86
using System;
7+
using System.IO;
8+
using Newtonsoft.Json;
99

10+
#nullable enable
1011
namespace Ipfs.Http
1112
{
1213
class PinApi : IPinApi
@@ -30,36 +31,162 @@ public async Task<IEnumerable<Cid>> AddAsync(string path, PinAddOptions options,
3031
optList.Add("name=" + options.Name);
3132
}
3233
var json = await ipfs.DoCommandAsync("pin/add", cancel, path, optList.ToArray());
33-
return ((JArray)JObject.Parse(json)["Pins"]) .Select(p => (Cid)(string)p);
34+
var dto = JsonConvert.DeserializeObject<PinChangeResponseDto>(json);
35+
var pins = dto?.Pins ?? new List<string>();
36+
return pins.Select(p => (Cid)p);
3437
}
3538

36-
public async Task<IEnumerable<Cid>> ListAsync(CancellationToken cancel = default)
39+
public async Task<IEnumerable<Cid>> AddAsync(string path, PinAddOptions options, IProgress<BlocksPinnedProgress> progress, CancellationToken cancel = default)
3740
{
38-
var json = await ipfs.DoCommandAsync("pin/ls", cancel);
39-
var keys = (JObject)(JObject.Parse(json)["Keys"]);
40-
return keys
41-
.Properties()
42-
.Select(p => (Cid)p.Name);
41+
options ??= new PinAddOptions();
42+
var optList = new List<string>
43+
{
44+
"recursive=" + options.Recursive.ToString().ToLowerInvariant(),
45+
"progress=true"
46+
};
47+
if (!string.IsNullOrEmpty(options.Name))
48+
{
49+
optList.Add("name=" + options.Name);
50+
}
51+
var pinned = new List<Cid>();
52+
var stream = await ipfs.PostDownloadAsync("pin/add", cancel, path, optList.ToArray());
53+
using var sr = new StreamReader(stream);
54+
while (!sr.EndOfStream && !cancel.IsCancellationRequested)
55+
{
56+
var line = await sr.ReadLineAsync();
57+
if (string.IsNullOrWhiteSpace(line))
58+
continue;
59+
var dto = JsonConvert.DeserializeObject<PinChangeResponseDto>(line);
60+
if (dto is null)
61+
continue;
62+
if (dto.Progress.HasValue)
63+
{
64+
progress?.Report(new BlocksPinnedProgress { BlocksPinned = dto.Progress.Value });
65+
}
66+
if (dto.Pins != null)
67+
{
68+
foreach (var p in dto.Pins)
69+
{
70+
pinned.Add((Cid)p);
71+
}
72+
}
73+
}
74+
return pinned;
4375
}
4476

45-
public async Task<IEnumerable<Cid>> ListAsync(PinType type, CancellationToken cancel = default(CancellationToken))
77+
public async IAsyncEnumerable<PinListItem> ListAsync([System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancel = default)
4678
{
47-
var typeOpt = type.ToString().ToLowerInvariant();
48-
var json = await ipfs.DoCommandAsync("pin/ls", cancel,
49-
null,
50-
$"type={typeOpt}");
51-
var keys = (JObject)(JObject.Parse(json)["Keys"]);
52-
return keys
53-
.Properties()
54-
.Select(p => (Cid)p.Name);
79+
// Default non-streaming, no names
80+
foreach (var item in await ListItemsOnceAsync(null, new List<string>(), cancel))
81+
{
82+
yield return item;
83+
}
84+
}
85+
86+
public async IAsyncEnumerable<PinListItem> ListAsync(PinType type, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancel = default)
87+
{
88+
var opts = new List<string> { $"type={type.ToString().ToLowerInvariant()}" };
89+
foreach (var item in await ListItemsOnceAsync(null, opts, cancel))
90+
{
91+
yield return item;
92+
}
93+
}
94+
95+
public async IAsyncEnumerable<PinListItem> ListAsync(PinListOptions options, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancel = default)
96+
{
97+
options ??= new PinListOptions();
98+
var opts = new List<string>();
99+
if (options.Type != PinType.All)
100+
opts.Add($"type={options.Type.ToString().ToLowerInvariant()}");
101+
if (!string.IsNullOrEmpty(options.Name))
102+
{
103+
opts.Add($"name={options.Name}");
104+
opts.Add("names=true");
105+
}
106+
else if (options.Names)
107+
{
108+
opts.Add("names=true");
109+
}
110+
111+
if (options.Stream)
112+
{
113+
await foreach (var item in ListItemsStreamAsync(null, opts, options.Names, cancel))
114+
{
115+
yield return item;
116+
}
117+
}
118+
else
119+
{
120+
foreach (var item in await ListItemsOnceAsync(null, opts, cancel))
121+
{
122+
yield return item;
123+
}
124+
}
55125
}
56126

57127
public async Task<IEnumerable<Cid>> RemoveAsync(Cid id, bool recursive = true, CancellationToken cancel = default(CancellationToken))
58128
{
59129
var opts = "recursive=" + recursive.ToString().ToLowerInvariant();
60130
var json = await ipfs.DoCommandAsync("pin/rm", cancel, id, opts);
61-
return ((JArray)JObject.Parse(json)["Pins"])
62-
.Select(p => (Cid)(string)p);
131+
var dto = JsonConvert.DeserializeObject<PinChangeResponseDto>(json);
132+
var pins = dto?.Pins ?? new List<string>();
133+
return pins.Select(p => (Cid)p);
134+
}
135+
136+
// Internal helper used by ListAsync overloads
137+
138+
async IAsyncEnumerable<PinListItem> ListItemsStreamAsync(string? path, List<string> opts, bool includeNames, [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancel)
139+
{
140+
opts = new List<string>(opts) { "stream=true" };
141+
var stream = await ipfs.PostDownloadAsync("pin/ls", cancel, path, opts.ToArray());
142+
using var sr = new StreamReader(stream);
143+
while (!sr.EndOfStream && !cancel.IsCancellationRequested)
144+
{
145+
var line = await sr.ReadLineAsync();
146+
if (string.IsNullOrWhiteSpace(line))
147+
continue;
148+
var dto = JsonConvert.DeserializeObject<PinLsObjectDto>(line);
149+
if (dto is null || string.IsNullOrEmpty(dto.Cid))
150+
continue;
151+
yield return new PinListItem
152+
{
153+
Cid = (Cid)dto.Cid!,
154+
Type = ParseType(dto.Type),
155+
Name = dto.Name
156+
};
157+
}
158+
}
159+
160+
async Task<IEnumerable<PinListItem>> ListItemsOnceAsync(string? path, List<string> opts, CancellationToken cancel)
161+
{
162+
var json = await ipfs.DoCommandAsync("pin/ls", cancel, path, opts.ToArray());
163+
var root = JsonConvert.DeserializeObject<PinListResponseDto>(json);
164+
var list = new List<PinListItem>();
165+
if (root?.Keys != null)
166+
{
167+
foreach (var kv in root.Keys)
168+
{
169+
list.Add(new PinListItem
170+
{
171+
Cid = (Cid)kv.Key!,
172+
Type = ParseType(kv.Value?.Type),
173+
Name = string.IsNullOrEmpty(kv.Value?.Name) ? null : kv.Value!.Name
174+
});
175+
}
176+
}
177+
return list;
178+
}
179+
180+
static PinType ParseType(string? t)
181+
{
182+
return t?.ToLowerInvariant() switch
183+
{
184+
"direct" => PinType.Direct,
185+
"indirect" => PinType.Indirect,
186+
"recursive" => PinType.Recursive,
187+
"all" => PinType.All,
188+
_ => PinType.All
189+
};
63190
}
64191

65192
}

src/CoreApi/PinDto.cs

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System.Collections.Generic;
2+
3+
#nullable enable
4+
namespace Ipfs.Http
5+
{
6+
/// <summary>
7+
/// Non-streaming response DTO for /api/v0/pin/ls.
8+
/// </summary>
9+
internal record PinListResponseDto
10+
{
11+
public Dictionary<string, PinInfoDto>? Keys { get; init; }
12+
}
13+
14+
/// <summary>
15+
/// DTO for entry value in PinListResponseDto.Keys.
16+
/// </summary>
17+
internal record PinInfoDto
18+
{
19+
public string? Name { get; init; }
20+
public string? Type { get; init; }
21+
}
22+
23+
/// <summary>
24+
/// Streaming response DTO for /api/v0/pin/ls?stream=true.
25+
/// </summary>
26+
internal record PinLsObjectDto
27+
{
28+
public string? Cid { get; init; }
29+
public string? Name { get; init; }
30+
public string? Type { get; init; }
31+
}
32+
33+
/// <summary>
34+
/// Response DTO for /api/v0/pin/add and /api/v0/pin/rm which both return a Pins array.
35+
/// </summary>
36+
internal record PinChangeResponseDto
37+
{
38+
public int? Progress { get; init; }
39+
public List<string>? Pins { get; init; }
40+
}
41+
}

src/IpfsHttpClient.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Added missing IFileSystemApi.ListAsync. Doesn't fully replace the removed IFileS
102102
</ItemGroup>
103103

104104
<ItemGroup>
105-
<PackageReference Include="IpfsShipyard.Ipfs.Core" Version="0.7.0" />
105+
<ProjectReference Include="..\..\net-ipfs-core\src\IpfsCore.csproj" />
106106
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
107107
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
108108
<PackageReference Include="Multiformats.Base" Version="2.0.2" />

test/AsyncEnumerableTestHelpers.cs

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Threading.Tasks;
4+
5+
namespace Ipfs.Http
6+
{
7+
internal static class AsyncEnumerableTestHelpers
8+
{
9+
public static IEnumerable<T> ToEnumerable<T>(this IAsyncEnumerable<T> source)
10+
{
11+
return source.ToArrayAsync().GetAwaiter().GetResult();
12+
}
13+
14+
public static async Task<T[]> ToArrayAsync<T>(this IAsyncEnumerable<T> source)
15+
{
16+
var list = new List<T>();
17+
await foreach (var item in source)
18+
{
19+
list.Add(item);
20+
}
21+
return list.ToArray();
22+
}
23+
}
24+
}

test/CoreApi/BlockApiTest.cs

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,13 @@ public void Put_Bytes_Pinned()
5959
{
6060
var data1 = new byte[] { 23, 24, 127 };
6161
var cid1 = ipfs.Block.PutAsync(data1, pin: true).Result;
62-
var pins = ipfs.Pin.ListAsync().Result;
63-
Assert.IsTrue(pins.Any(pin => pin == cid1.Id));
62+
var pins = ipfs.Pin.ListAsync().ToEnumerable();
63+
Assert.IsTrue(pins.Any(pin => pin.Cid == cid1.Id));
6464

6565
var data2 = new byte[] { 123, 124, 27 };
6666
var cid2 = ipfs.Block.PutAsync(data2, pin: false).Result;
67-
pins = ipfs.Pin.ListAsync().Result;
68-
Assert.IsFalse(pins.Any(pin => pin == cid2.Id));
67+
pins = ipfs.Pin.ListAsync().ToEnumerable();
68+
Assert.IsFalse(pins.Any(pin => pin.Cid == cid2.Id));
6969
}
7070

7171
[TestMethod]
@@ -106,13 +106,13 @@ public void Put_Stream_Pinned()
106106
{
107107
var data1 = new MemoryStream(new byte[] { 23, 24, 127 });
108108
var cid1 = ipfs.Block.PutAsync(data1, pin: true).Result;
109-
var pins = ipfs.Pin.ListAsync().Result;
110-
Assert.IsTrue(pins.Any(pin => pin == cid1.Id));
109+
var pins = ipfs.Pin.ListAsync().ToEnumerable();
110+
Assert.IsTrue(pins.Any(pin => pin.Cid == cid1.Id));
111111

112112
var data2 = new MemoryStream(new byte[] { 123, 124, 27 });
113113
var cid2 = ipfs.Block.PutAsync(data2, pin: false).Result;
114-
pins = ipfs.Pin.ListAsync().Result;
115-
Assert.IsFalse(pins.Any(pin => pin == cid2.Id));
114+
pins = ipfs.Pin.ListAsync().ToEnumerable();
115+
Assert.IsFalse(pins.Any(pin => pin.Cid == cid2.Id));
116116
}
117117

118118
[TestMethod]

test/CoreApi/FileSystemApiTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,8 @@ public void Add_NoPin()
9595
var data = new MemoryStream(new byte[] { 11, 22, 33 });
9696
var options = new AddFileOptions { Pin = false };
9797
var node = ipfs.FileSystem.AddAsync(data, "", options).Result;
98-
var pins = ipfs.Pin.ListAsync().Result;
99-
Assert.IsFalse(pins.Any(pin => pin == node.Id));
98+
var pins = ipfs.Pin.ListAsync().ToEnumerable();
99+
Assert.IsFalse(pins.Any(pin => pin.Cid == node.Id));
100100
}
101101

102102
[TestMethod]

0 commit comments

Comments
 (0)