Skip to content

Commit b1edba4

Browse files
committed
dag: use POST for export; import tolerant to missing lines on older Kubo; add export/import test; fix dag import tests to use Root; test fixture uses temp for kubo bin + working dir fallback
1 parent 3b280d9 commit b1edba4

File tree

3 files changed

+91
-14
lines changed

3 files changed

+91
-14
lines changed

src/CoreApi/DagApi.cs

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -154,23 +154,32 @@ public async Task<DagStatSummary> StatAsync(string cid, IProgress<DagStatSummary
154154

155155
public Task<Stream> ExportAsync(string path, CancellationToken cancellationToken = default)
156156
{
157-
return ipfs.DownloadAsync("dag/export", cancellationToken, path);
157+
// Kubo expects POST for dag/export
158+
return ipfs.PostDownloadAsync("dag/export", cancellationToken, path);
158159
}
159160

160161
public async Task<CarImportOutput> ImportAsync(Stream stream, bool? pinRoots = null, bool stats = false, CancellationToken cancellationToken = default)
161162
{
162-
string[] options = [
163-
$"pin-roots={pinRoots.ToString().ToLowerInvariant()}",
164-
$"stats={stats.ToString().ToLowerInvariant()}"
165-
];
163+
// Respect Kubo default (pin roots = true) by omitting the flag when null.
164+
var optionsList = new System.Collections.Generic.List<string>();
165+
if (pinRoots.HasValue)
166+
optionsList.Add($"pin-roots={pinRoots.Value.ToString().ToLowerInvariant()}");
167+
168+
optionsList.Add($"stats={stats.ToString().ToLowerInvariant()}");
169+
var options = optionsList.ToArray();
166170

167171
using var resultStream = await ipfs.Upload2Async("dag/import", cancellationToken, stream, null, options);
168172

169173
// Read line-by-line
170174
using var reader = new StreamReader(resultStream);
171175

172-
// First output is always of type CarImportOutput
176+
// First output line may be absent on older Kubo when pin-roots=false
173177
var json = await reader.ReadLineAsync();
178+
if (string.IsNullOrEmpty(json))
179+
{
180+
return new CarImportOutput();
181+
}
182+
174183
var res = JsonConvert.DeserializeObject<CarImportOutput>(json);
175184
if (res is null)
176185
throw new InvalidDataException($"The response did not deserialize to {nameof(CarImportOutput)}.");
@@ -179,11 +188,14 @@ public async Task<CarImportOutput> ImportAsync(Stream stream, bool? pinRoots = n
179188
if (stats)
180189
{
181190
json = await reader.ReadLineAsync();
182-
var importStats = JsonConvert.DeserializeObject<CarImportStats>(json);
183-
if (importStats is null)
184-
throw new InvalidDataException($"The response did not deserialize a {nameof(CarImportStats)}.");
191+
if (!string.IsNullOrEmpty(json))
192+
{
193+
var importStats = JsonConvert.DeserializeObject<CarImportStats>(json);
194+
if (importStats is null)
195+
throw new InvalidDataException($"The response did not deserialize a {nameof(CarImportStats)}.");
185196

186-
res.Stats = importStats;
197+
res.Stats = importStats;
198+
}
187199
}
188200

189201
return res;

test/CoreApi/DagApiTest.cs

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Microsoft.VisualStudio.TestTools.UnitTesting;
22
using Newtonsoft.Json.Linq;
3+
using System.Linq;
34
using System.Threading.Tasks;
45

56
namespace Ipfs.Http
@@ -49,6 +50,64 @@ public async Task PutAndGet_POCO()
4950
var value = (string)await ipfs.Dag.GetAsync(id.Encode() + "/Last");
5051
Assert.AreEqual(expected.Last, value);
5152
}
53+
54+
[TestMethod]
55+
public async Task Import_Default_Pins_Roots()
56+
{
57+
var ipfs = TestFixture.Ipfs;
58+
59+
var node = await ipfs.FileSystem.AddTextAsync("car import default pin");
60+
await using var car = await ipfs.Dag.ExportAsync(node.Id);
61+
62+
// ensure unpinned first
63+
await ipfs.Pin.RemoveAsync(node.Id);
64+
65+
var result = await ipfs.Dag.ImportAsync(car, pinRoots: null, stats: false);
66+
Assert.IsNotNull(result.Root);
67+
68+
var pins = await ipfs.Pin.ListAsync().ToArrayAsync();
69+
Assert.IsTrue(pins.Any(p => p.Cid == node.Id));
70+
}
71+
72+
[TestMethod]
73+
public async Task Import_PinRoots_False_Does_Not_Pin()
74+
{
75+
var ipfs = TestFixture.Ipfs;
76+
77+
var node = await ipfs.FileSystem.AddTextAsync("car import nopin");
78+
await using var car = await ipfs.Dag.ExportAsync(node.Id);
79+
80+
// ensure unpinned first
81+
await ipfs.Pin.RemoveAsync(node.Id);
82+
83+
var result = await ipfs.Dag.ImportAsync(car, pinRoots: false, stats: false);
84+
// Some Kubo versions emit no Root output when pin-roots=false; allow null.
85+
86+
var pins = await ipfs.Pin.ListAsync().ToArrayAsync();
87+
Assert.IsFalse(pins.Any(p => p.Cid == node.Id));
88+
}
89+
90+
[TestMethod]
91+
public async Task Export_Then_Import_Roundtrip_Preserves_Root()
92+
{
93+
var ipfs = TestFixture.Ipfs;
94+
95+
var node = await ipfs.FileSystem.AddTextAsync("car export roundtrip");
96+
97+
// ensure unpinned first so import with pinRoots=true creates a new pin
98+
try { await ipfs.Pin.RemoveAsync(node.Id); } catch { }
99+
100+
await using var car = await ipfs.Dag.ExportAsync(node.Id);
101+
Assert.IsNotNull(car);
102+
103+
var result = await ipfs.Dag.ImportAsync(car, pinRoots: true, stats: false);
104+
Assert.IsNotNull(result.Root);
105+
Assert.AreEqual(node.Id.ToString(), result.Root!.Cid.ToString());
106+
107+
// Verify it is pinned now
108+
var pins = await ipfs.Pin.ListAsync().ToArrayAsync();
109+
Assert.IsTrue(pins.Any(p => p.Cid == node.Id));
110+
}
52111
}
53112
}
54113

test/TestFixture.cs

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,12 @@ public static void AssemblyInit(TestContext context)
2525
{
2626
OwlCore.Diagnostics.Logger.MessageReceived += (sender, args) => context.WriteLine(args.Message);
2727

28-
// Ensure the test runner has provided a deployment directory to use for working folders.
29-
Assert.IsNotNull(context.DeploymentDirectory);
28+
// Prefer the deployment directory when provided; otherwise fall back to a temp folder.
29+
var deploymentDir = context.DeploymentDirectory ?? Path.Combine(Path.GetTempPath(), "IpfsHttpClientTests", "Work");
30+
Directory.CreateDirectory(deploymentDir);
3031

3132
// Create a working folder and start a fresh Kubo node with default bootstrap peers.
32-
var workingFolder = SafeCreateWorkingFolder(new SystemFolder(context.DeploymentDirectory), typeof(TestFixture).Namespace ?? "test").GetAwaiter().GetResult();
33+
var workingFolder = SafeCreateWorkingFolder(new SystemFolder(deploymentDir), typeof(TestFixture).Namespace ?? "test").GetAwaiter().GetResult();
3334

3435
// Use non-default ports to avoid conflicts with any locally running node.
3536
int apiPort = 11501;
@@ -52,13 +53,18 @@ public static async Task<KuboBootstrapper> CreateNodeAsync(SystemFolder workingD
5253
{
5354
var nodeRepo = (SystemFolder)await workingDirectory.CreateFolderAsync(nodeRepoName, overwrite: true);
5455

56+
// Use a temp folder for the Kubo binary cache to avoid limited space on the deployment drive.
57+
var binCachePath = Path.Combine(Path.GetTempPath(), "IpfsHttpClientTests", "KuboBin");
58+
Directory.CreateDirectory(binCachePath);
59+
var binCacheFolder = new SystemFolder(binCachePath);
60+
5561
var node = new KuboBootstrapper(nodeRepo.Path)
5662
{
5763
ApiUri = new Uri($"http://127.0.0.1:{apiPort}"),
5864
GatewayUri = new Uri($"http://127.0.0.1:{gatewayPort}"),
5965
RoutingMode = DhtRoutingMode.AutoClient,
6066
LaunchConflictMode = BootstrapLaunchConflictMode.Relaunch,
61-
BinaryWorkingFolder = workingDirectory,
67+
BinaryWorkingFolder = binCacheFolder,
6268
EnableFilestore = true,
6369
};
6470

0 commit comments

Comments
 (0)