Skip to content

Commit eb3e2c1

Browse files
committed
Fix key ordering in cbor maps
Improve error handling and reporting Fix refs to read stream of objects Restrict block put to a single file Add more tests
1 parent 17793bb commit eb3e2c1

File tree

4 files changed

+158
-46
lines changed

4 files changed

+158
-46
lines changed

src/main/java/io/ipfs/api/IPFS.java

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -110,8 +110,12 @@ public InputStream catStream(Multihash hash) throws IOException {
110110
return retrieveStream("cat/" + hash);
111111
}
112112

113-
public Map refs(Multihash hash, boolean recursive) throws IOException {
114-
return retrieveMap("refs?arg=" + hash +"&r="+recursive);
113+
public List<Multihash> refs(Multihash hash, boolean recursive) throws IOException {
114+
String jsonStream = new String(retrieve("refs?arg=" + hash + "&r=" + recursive));
115+
return JSONParser.parseStream(jsonStream).stream()
116+
.map(m -> (String) (((Map) m).get("Ref")))
117+
.map(Cid::decode)
118+
.collect(Collectors.toList());
115119
}
116120

117121
public Map resolve(String scheme, Multihash hash, boolean recursive) throws IOException {
@@ -150,7 +154,7 @@ public class Pin {
150154
public List<Multihash> add(Multihash hash) throws IOException {
151155
return ((List<Object>)((Map)retrieveAndParse("pin/add?stream-channels=true&arg=" + hash)).get("Pins"))
152156
.stream()
153-
.map(x -> Cid.decode((String)x))
157+
.map(x -> Cid.decode((String) x))
154158
.collect(Collectors.toList());
155159
}
156160

@@ -194,12 +198,16 @@ public List<MerkleNode> put(List<byte[]> data) throws IOException {
194198
}
195199

196200
public List<MerkleNode> put(List<byte[]> data, Optional<String> format) throws IOException {
201+
// N.B. Once IPFS implements a bulk put this can become a single multipart call with multiple 'files'
202+
return data.stream().map(array -> put(array, format)).collect(Collectors.toList());
203+
}
204+
205+
public MerkleNode put(byte[] data, Optional<String> format) {
197206
String fmt = format.map(f -> "&format=" + f).orElse("");
198207
Multipart m = new Multipart("http://" + host + ":" + port + version+"block/put?stream-channels=true" + fmt, "UTF-8");
199-
for (byte[] f : data)
200-
m.addFilePart("file", new NamedStreamable.ByteArrayWrapper(f));
208+
m.addFilePart("file", new NamedStreamable.ByteArrayWrapper(data));
201209
String res = m.finish();
202-
return JSONParser.parseStream(res).stream().map(x -> MerkleNode.fromJSON((Map<String, Object>) x)).collect(Collectors.toList());
210+
return JSONParser.parseStream(res).stream().map(x -> MerkleNode.fromJSON((Map<String, Object>) x)).findFirst().get();
203211
}
204212

205213
public Map stat(Multihash hash) throws IOException {
@@ -506,7 +514,8 @@ private static byte[] get(URL target) throws IOException {
506514
} catch (ConnectException e) {
507515
throw new RuntimeException("Couldn't connect to IPFS daemon at "+target+"\n Is IPFS running?");
508516
} catch (IOException e) {
509-
throw new RuntimeException("IOException contacting IPFS daemon.\nTrailer: " + conn.getHeaderFields().get("Trailer"), e);
517+
String err = new String(readFully(conn.getErrorStream()));
518+
throw new RuntimeException("IOException contacting IPFS daemon.\nTrailer: " + conn.getHeaderFields().get("Trailer") + " " + err, e);
510519
}
511520
}
512521

@@ -541,6 +550,10 @@ private static byte[] post(URL target, byte[] body, Map<String, String> headers)
541550
out.close();
542551

543552
InputStream in = conn.getInputStream();
553+
return readFully(in);
554+
}
555+
556+
private static final byte[] readFully(InputStream in) throws IOException {
544557
ByteArrayOutputStream resp = new ByteArrayOutputStream();
545558
byte[] buf = new byte[4096];
546559
int r;

src/main/java/io/ipfs/api/Multipart.java

Lines changed: 49 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -12,20 +12,24 @@ public class Multipart {
1212
private OutputStream out;
1313
private PrintWriter writer;
1414

15-
public Multipart(String requestURL, String charset) throws IOException {
15+
public Multipart(String requestURL, String charset) {
1616
this.charset = charset;
1717

1818
boundary = createBoundary();
1919

20-
URL url = new URL(requestURL);
21-
httpConn = (HttpURLConnection) url.openConnection();
22-
httpConn.setUseCaches(false);
23-
httpConn.setDoOutput(true);
24-
httpConn.setDoInput(true);
25-
httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
26-
httpConn.setRequestProperty("User-Agent", "Java IPFS CLient");
27-
out = httpConn.getOutputStream();
28-
writer = new PrintWriter(new OutputStreamWriter(out, charset), true);
20+
try {
21+
URL url = new URL(requestURL);
22+
httpConn = (HttpURLConnection) url.openConnection();
23+
httpConn.setUseCaches(false);
24+
httpConn.setDoOutput(true);
25+
httpConn.setDoInput(true);
26+
httpConn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
27+
httpConn.setRequestProperty("User-Agent", "Java IPFS CLient");
28+
out = httpConn.getOutputStream();
29+
writer = new PrintWriter(new OutputStreamWriter(out, charset), true);
30+
} catch (IOException e) {
31+
throw new RuntimeException(e.getMessage(), e);
32+
}
2933
}
3034

3135
public static String createBoundary() {
@@ -73,25 +77,29 @@ public void addDirectoryPart(String path) {
7377
}
7478
}
7579

76-
public void addFilePart(String fieldName, NamedStreamable uploadFile) throws IOException {
80+
public void addFilePart(String fieldName, NamedStreamable uploadFile) {
7781
Optional<String> fileName = uploadFile.getName();
7882
writer.append("--" + boundary).append(LINE_FEED);
7983
if (!fileName.isPresent())
8084
writer.append("Content-Disposition: file; name=\"" + fieldName + "\";").append(LINE_FEED);
8185
else
82-
writer.append("Content-Disposition: file; filename=\"" + fileName.get() + "\"").append(LINE_FEED);
86+
writer.append("Content-Disposition: file; filename=\"" + fileName.get() + "\";").append(LINE_FEED);
8387
writer.append("Content-Type: application/octet-stream").append(LINE_FEED);
8488
writer.append("Content-Transfer-Encoding: binary").append(LINE_FEED);
8589
writer.append(LINE_FEED);
8690
writer.flush();
8791

88-
InputStream inputStream = uploadFile.getInputStream();
89-
byte[] buffer = new byte[4096];
90-
int r;
91-
while ((r = inputStream.read(buffer)) != -1)
92-
out.write(buffer, 0, r);
93-
out.flush();
94-
inputStream.close();
92+
try {
93+
InputStream inputStream = uploadFile.getInputStream();
94+
byte[] buffer = new byte[4096];
95+
int r;
96+
while ((r = inputStream.read(buffer)) != -1)
97+
out.write(buffer, 0, r);
98+
out.flush();
99+
inputStream.close();
100+
} catch (IOException e) {
101+
throw new RuntimeException(e.getMessage(), e);
102+
}
95103

96104
writer.append(LINE_FEED);
97105
writer.flush();
@@ -102,35 +110,40 @@ public void addHeaderField(String name, String value) {
102110
writer.flush();
103111
}
104112

105-
public String finish() throws IOException {
113+
public String finish() {
106114
StringBuilder b = new StringBuilder();
107115

108116
writer.append("--" + boundary + "--").append(LINE_FEED);
109117
writer.close();
110118

111-
int status = httpConn.getResponseCode();
112-
if (status == HttpURLConnection.HTTP_OK) {
113-
BufferedReader reader = new BufferedReader(new InputStreamReader(
114-
httpConn.getInputStream()));
115-
String line;
116-
while ((line = reader.readLine()) != null) {
117-
b.append(line);
118-
}
119-
reader.close();
120-
httpConn.disconnect();
121-
} else {
122-
try {
119+
try {
120+
int status = httpConn.getResponseCode();
121+
if (status == HttpURLConnection.HTTP_OK) {
123122
BufferedReader reader = new BufferedReader(new InputStreamReader(
124123
httpConn.getInputStream()));
125124
String line;
126125
while ((line = reader.readLine()) != null) {
127126
b.append(line);
128127
}
129128
reader.close();
130-
} catch (Throwable t) {}
131-
throw new IOException("Server returned status: " + status + " with body: "+b.toString() + " and Trailer header: "+httpConn.getHeaderFields().get("Trailer"));
132-
}
129+
httpConn.disconnect();
130+
} else {
131+
try {
132+
BufferedReader reader = new BufferedReader(new InputStreamReader(
133+
httpConn.getInputStream()));
134+
String line;
135+
while ((line = reader.readLine()) != null) {
136+
b.append(line);
137+
}
138+
reader.close();
139+
} catch (Throwable t) {
140+
}
141+
throw new IOException("Server returned status: " + status + " with body: " + b.toString() + " and Trailer header: " + httpConn.getHeaderFields().get("Trailer"));
142+
}
133143

134-
return b.toString();
144+
return b.toString();
145+
} catch (IOException e) {
146+
throw new RuntimeException(e.getMessage(), e);
147+
}
135148
}
136149
}

src/main/java/io/ipfs/api/cbor/CborObject.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,9 @@ public CborString(String value) {
305305

306306
@Override
307307
public int compareTo(CborString cborString) {
308+
int lenDiff = value.length() - cborString.value.length();
309+
if (lenDiff != 0)
310+
return lenDiff;
308311
return value.compareTo(cborString.value);
309312
}
310313

src/test/java/io/ipfs/api/APITest.java

Lines changed: 86 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,20 @@ public void blockTest() {
309309
}
310310
}
311311

312+
@org.junit.Test
313+
public void bulkBlockTest() {
314+
try {
315+
CborObject cbor = new CborObject.CborString("G'day IPFS!");
316+
byte[] raw = cbor.toByteArray();
317+
List<MerkleNode> bulkPut = ipfs.block.put(Arrays.asList(raw, raw, raw, raw, raw), Optional.of("cbor"));
318+
List<Multihash> hashes = bulkPut.stream().map(m -> m.hash).collect(Collectors.toList());
319+
byte[] result = ipfs.block.get(hashes.get(0));
320+
System.out.println();
321+
} catch (IOException e) {
322+
throw new RuntimeException(e);
323+
}
324+
}
325+
312326
private static String toEscapedHex(byte[] in) throws IOException {
313327
StringBuilder res = new StringBuilder();
314328
for (byte b : in) {
@@ -325,20 +339,27 @@ private static String toEscapedHex(byte[] in) throws IOException {
325339
public void merkleLinkInMap() {
326340
try {
327341
Random r = new Random();
328-
CborObject.CborByteArray target = new CborObject.CborByteArray(("g'day IPFS!" + r.nextInt()).getBytes());
342+
CborObject.CborByteArray target = new CborObject.CborByteArray(("g'day IPFS!").getBytes());
329343
byte[] rawTarget = target.toByteArray();
330344
MerkleNode targetRes = ipfs.block.put(Arrays.asList(rawTarget), Optional.of("cbor")).get(0);
331345

332346
CborObject.CborMerkleLink link = new CborObject.CborMerkleLink(targetRes.hash);
333-
CborObject.CborMap source = CborObject.CborMap.build(Stream.of(link)
334-
.collect(Collectors.toMap(l -> l.target.toString(), l -> l)));
347+
Map<String, CborObject> m = new TreeMap<>();
348+
m.put("alink", link);
349+
m.put("arr", new CborObject.CborList(Collections.emptyList()));
350+
CborObject.CborMap source = CborObject.CborMap.build(m);
335351
byte[] rawSource = source.toByteArray();
336352
MerkleNode sourceRes = ipfs.block.put(Arrays.asList(rawSource), Optional.of("cbor")).get(0);
337353

354+
CborObject.fromByteArray(rawSource);
355+
338356
List<Multihash> add = ipfs.pin.add(sourceRes.hash);
339357
ipfs.repo.gc();
340358
ipfs.repo.gc();
341359

360+
List<Multihash> refs = ipfs.refs(sourceRes.hash, true);
361+
Assert.assertTrue("refs returns links", refs.contains(targetRes.hash));
362+
342363
byte[] bytes = ipfs.block.get(targetRes.hash);
343364
Assert.assertTrue("same contents after GC", Arrays.equals(bytes, rawTarget));
344365
// These commands can be used to reproduce this on the command line
@@ -350,6 +371,44 @@ public void merkleLinkInMap() {
350371
}
351372
}
352373

374+
@org.junit.Test
375+
public void recursiveRefs() {
376+
try {
377+
CborObject.CborByteArray leaf1 = new CborObject.CborByteArray(("G'day IPFS!").getBytes());
378+
byte[] rawLeaf1 = leaf1.toByteArray();
379+
MerkleNode leaf1Res = ipfs.block.put(Arrays.asList(rawLeaf1), Optional.of("cbor")).get(0);
380+
381+
CborObject.CborMerkleLink link = new CborObject.CborMerkleLink(leaf1Res.hash);
382+
Map<String, CborObject> m = new TreeMap<>();
383+
m.put("link1", link);
384+
CborObject.CborMap source = CborObject.CborMap.build(m);
385+
MerkleNode sourceRes = ipfs.block.put(Arrays.asList(source.toByteArray()), Optional.of("cbor")).get(0);
386+
387+
CborObject.CborByteArray leaf2 = new CborObject.CborByteArray(("G'day again, IPFS!").getBytes());
388+
byte[] rawLeaf2 = leaf2.toByteArray();
389+
MerkleNode leaf2Res = ipfs.block.put(Arrays.asList(rawLeaf2), Optional.of("cbor")).get(0);
390+
391+
Map<String, CborObject> m2 = new TreeMap<>();
392+
m2.put("link1", new CborObject.CborMerkleLink(sourceRes.hash));
393+
m2.put("link2", new CborObject.CborMerkleLink(leaf2Res.hash));
394+
CborObject.CborMap source2 = CborObject.CborMap.build(m2);
395+
MerkleNode rootRes = ipfs.block.put(Arrays.asList(source2.toByteArray()), Optional.of("cbor")).get(0);
396+
397+
List<Multihash> refs = ipfs.refs(rootRes.hash, false);
398+
boolean correct = refs.contains(sourceRes.hash) && refs.contains(leaf2Res.hash) && refs.size() == 2;
399+
Assert.assertTrue("refs returns links", correct);
400+
401+
List<Multihash> refsRecurse = ipfs.refs(rootRes.hash, true);
402+
boolean correctRecurse = refs.contains(sourceRes.hash)
403+
&& refs.contains(leaf1Res.hash)
404+
&& refs.contains(leaf2Res.hash)
405+
&& refs.size() == 3;
406+
Assert.assertTrue("refs returns links", correct);
407+
} catch (IOException e) {
408+
throw new RuntimeException(e);
409+
}
410+
}
411+
353412
/**
354413
* Test that merkle links as a root object are followed during recursive pins
355414
*/
@@ -385,6 +444,30 @@ public void rootMerkleLink() {
385444
}
386445
}
387446

447+
/**
448+
* Test that merkle links as a root object are followed during recursive pins
449+
*/
450+
@org.junit.Test
451+
public void rootNull() {
452+
try {
453+
CborObject.CborNull cbor = new CborObject.CborNull();
454+
byte[] obj = cbor.toByteArray();
455+
MerkleNode block = ipfs.block.put(Arrays.asList(obj), Optional.of("cbor")).get(0);
456+
byte[] retrievedObj = ipfs.block.get(block.hash);
457+
Assert.assertTrue("get inverse of put", Arrays.equals(retrievedObj, obj));
458+
459+
List<Multihash> add = ipfs.pin.add(block.hash);
460+
ipfs.repo.gc();
461+
ipfs.repo.gc();
462+
463+
// These commands can be used to reproduce this on the command line
464+
String reproCommand1 = "printf \"" + toEscapedHex(obj) + "\" | ipfs block put --format=cbor";
465+
System.out.println();
466+
} catch (IOException e) {
467+
throw new RuntimeException(e);
468+
}
469+
}
470+
388471
/**
389472
* Test that merkle links in a cbor list are followed during recursive pins
390473
*/

0 commit comments

Comments
 (0)