github.com/ipni/storetheindex@v0.8.30/carstore/carwriter_test.go (about) 1 package carstore_test 2 3 import ( 4 "bytes" 5 "compress/gzip" 6 "context" 7 "io" 8 "io/fs" 9 "testing" 10 11 "github.com/ipfs/go-datastore" 12 car "github.com/ipld/go-car/v2" 13 carblockstore "github.com/ipld/go-car/v2/blockstore" 14 carindex "github.com/ipld/go-car/v2/index" 15 "github.com/ipld/go-ipld-prime" 16 cidlink "github.com/ipld/go-ipld-prime/linking/cid" 17 "github.com/ipni/go-libipni/ingest/schema" 18 "github.com/ipni/go-libipni/test" 19 "github.com/ipni/storetheindex/carstore" 20 "github.com/ipni/storetheindex/filestore" 21 crypto "github.com/libp2p/go-libp2p/core/crypto" 22 "github.com/libp2p/go-libp2p/core/peer" 23 p2ptest "github.com/libp2p/go-libp2p/core/test" 24 "github.com/multiformats/go-multicodec" 25 "github.com/multiformats/go-multihash" 26 "github.com/stretchr/testify/require" 27 ) 28 29 const ( 30 testCompress = carstore.Gzip 31 32 testEntriesChunkCount = 3 33 testEntriesChunkSize = 15 34 ) 35 36 func TestWrite(t *testing.T) { 37 const entBlockCount = 5 38 39 dstore := datastore.NewMapDatastore() 40 metadata := []byte("car-test-metadata") 41 42 carDir := t.TempDir() 43 fileStore, err := filestore.NewLocal(carDir) 44 require.NoError(t, err) 45 carw, err := carstore.NewWriter(dstore, fileStore, carstore.WithCompress(testCompress)) 46 require.NoError(t, err) 47 48 adLink, ad, _, _, _ := storeRandomIndexAndAd(t, entBlockCount, metadata, nil, dstore) 49 adCid := adLink.(cidlink.Link).Cid 50 entriesCid := ad.Entries.(cidlink.Link).Cid 51 52 ctx := context.Background() 53 54 // Check that datastore has ad and entries CID before reading to car. 55 ok, err := dstore.Has(ctx, datastore.NewKey(adCid.String())) 56 require.NoError(t, err) 57 require.True(t, ok) 58 ok, err = dstore.Has(ctx, datastore.NewKey(entriesCid.String())) 59 require.NoError(t, err) 60 require.True(t, ok) 61 62 // Test that car file is created. 63 carInfo, err := carw.Write(ctx, adCid, false, false) 64 require.NoError(t, err) 65 require.NotNil(t, carInfo) 66 headInfo, err := fileStore.Head(ctx, carInfo.Path) 67 require.NoError(t, err) 68 require.Equal(t, carInfo.Path, headInfo.Path) 69 require.Equal(t, carInfo.Size, headInfo.Size) 70 t.Log("Created advertisement CAR file:", carInfo.Path) 71 72 // Read CAR file and see that it has expected contents. 73 _, r, err := fileStore.Get(ctx, carInfo.Path) 74 require.NoError(t, err) 75 var buf bytes.Buffer 76 _, err = io.Copy(&buf, r) 77 require.NoError(t, err) 78 err = r.Close() 79 require.NoError(t, err) 80 81 if carw.Compression() == carstore.Gzip { 82 gzr, err := gzip.NewReader(&buf) 83 require.NoError(t, err) 84 var ungzBuf bytes.Buffer 85 _, err = io.Copy(&ungzBuf, gzr) 86 require.NoError(t, err) 87 gzr.Close() 88 buf = ungzBuf 89 } 90 91 reader := bytes.NewReader(buf.Bytes()) 92 93 cbs, err := carblockstore.NewReadOnly(reader, nil) 94 require.NoError(t, err) 95 96 // Check that ad block is present. 97 blk, err := cbs.Get(ctx, adCid) 98 require.NoError(t, err, "failed to get ad block from car file") 99 require.NotNil(t, blk) 100 101 // Check that first entries block is present. 102 blk, err = cbs.Get(ctx, entriesCid) 103 require.NoError(t, err, "failed to get ad entried block from car file") 104 require.NotNil(t, blk) 105 106 // Check that the CAR is iterable. 107 reader.Reset(buf.Bytes()) 108 cr, err := car.NewReader(reader) 109 require.NoError(t, err) 110 defer cr.Close() 111 112 idxReader, err := cr.IndexReader() 113 require.NoError(t, err) 114 require.NotNil(t, idxReader, "CAR has no index") 115 116 idx, err := carindex.ReadFrom(idxReader) 117 require.NoError(t, err) 118 119 codec := idx.Codec() 120 t.Log("CAR codec:", codec) 121 require.Equal(t, multicodec.CarMultihashIndexSorted, codec, "CAR index not iterable, wrong codec") 122 itIdx, ok := idx.(carindex.IterableIndex) 123 require.True(t, ok, "expected CAR index to implement index.IterableIndex interface") 124 125 offset, err := carindex.GetFirst(itIdx, entriesCid) 126 require.NoError(t, err) 127 require.NotZero(t, offset) 128 129 // Check that there is 1 ad and 5 entries chunks stored. 130 var count int 131 err = itIdx.ForEach(func(mh multihash.Multihash, offset uint64) error { 132 count++ 133 return nil 134 }) 135 require.NoError(t, err) 136 require.Equal(t, 1+entBlockCount, count) 137 138 // Check that ad and entries block are no longer in datastore. 139 ok, err = dstore.Has(ctx, datastore.NewKey(adCid.String())) 140 require.NoError(t, err) 141 require.False(t, ok) 142 ok, err = dstore.Has(ctx, datastore.NewKey(entriesCid.String())) 143 require.NoError(t, err) 144 require.False(t, ok) 145 } 146 147 func TestWriteToExistingAdCar(t *testing.T) { 148 const entBlockCount = 1 149 150 dstore := datastore.NewMapDatastore() 151 metadata := []byte("car-test-metadata") 152 153 adLink, ad, _, _, _ := storeRandomIndexAndAd(t, entBlockCount, metadata, nil, dstore) 154 adCid := adLink.(cidlink.Link).Cid 155 entriesCid := ad.Entries.(cidlink.Link).Cid 156 157 ctx := context.Background() 158 159 // Check that datastore has ad and entries CID before reading to car. 160 ok, err := dstore.Has(ctx, datastore.NewKey(adCid.String())) 161 require.NoError(t, err) 162 require.True(t, ok) 163 ok, err = dstore.Has(ctx, datastore.NewKey(entriesCid.String())) 164 require.NoError(t, err) 165 require.True(t, ok) 166 167 fileStore, err := filestore.NewLocal(t.TempDir()) 168 require.NoError(t, err) 169 170 fileName := adCid.String() + carstore.CarFileSuffix 171 if testCompress == carstore.Gzip { 172 fileName += carstore.GzipFileSuffix 173 } 174 175 _, err = fileStore.Put(ctx, fileName, nil) 176 require.NoError(t, err) 177 178 carw, err := carstore.NewWriter(dstore, fileStore, carstore.WithCompress(testCompress)) 179 require.NoError(t, err) 180 181 carInfo, err := carw.Write(ctx, adCid, false, true) 182 require.ErrorIs(t, err, fs.ErrExist) 183 require.Zero(t, carInfo.Size) 184 185 // Check that ad car file was not written to. 186 fileInfo, err := fileStore.Head(ctx, carInfo.Path) 187 require.NoError(t, err) 188 require.Zero(t, fileInfo.Size) 189 190 // Check that ad and entries block are no longer in datastore. 191 ok, err = dstore.Has(ctx, datastore.NewKey(adCid.String())) 192 require.NoError(t, err) 193 require.False(t, ok) 194 ok, err = dstore.Has(ctx, datastore.NewKey(entriesCid.String())) 195 require.NoError(t, err) 196 require.False(t, ok) 197 } 198 199 func TestWriteChain(t *testing.T) { 200 const entBlockCount = 5 201 202 dstore := datastore.NewMapDatastore() 203 metadata := []byte("car-test-metadata") 204 205 carDir := t.TempDir() 206 fileStore, err := filestore.NewLocal(carDir) 207 require.NoError(t, err) 208 carw, err := carstore.NewWriter(dstore, fileStore, carstore.WithCompress(testCompress)) 209 require.NoError(t, err) 210 211 adLink1, _, _, _, _ := storeRandomIndexAndAd(t, entBlockCount, metadata, nil, dstore) 212 adLink2, _, _, _, _ := storeRandomIndexAndAd(t, entBlockCount, metadata, adLink1, dstore) 213 adCid2 := adLink2.(cidlink.Link).Cid 214 215 ctx := context.Background() 216 217 count, err := carw.WriteChain(ctx, adCid2, false) 218 require.NoError(t, err) 219 require.Equal(t, 2, count) 220 221 // Test that car file is created. 222 fileCh, errCh := fileStore.List(ctx, "", false) 223 infos := make([]*filestore.File, 0, 2) 224 for fileInfo := range fileCh { 225 infos = append(infos, fileInfo) 226 } 227 err = <-errCh 228 require.NoError(t, err) 229 require.Equal(t, 2, len(infos)) 230 } 231 232 func newRandomLinkedList(t *testing.T, lsys ipld.LinkSystem, size int) (ipld.Link, []multihash.Multihash) { 233 var out []multihash.Multihash 234 var nextLnk ipld.Link 235 for i := 0; i < size; i++ { 236 mhs := test.RandomMultihashes(testEntriesChunkSize) 237 chunk := &schema.EntryChunk{ 238 Entries: mhs, 239 Next: nextLnk, 240 } 241 node, err := chunk.ToNode() 242 require.NoError(t, err) 243 lnk, err := lsys.Store(ipld.LinkContext{}, schema.Linkproto, node) 244 require.NoError(t, err) 245 out = append(out, mhs...) 246 nextLnk = lnk 247 } 248 return nextLnk, out 249 } 250 251 func mkProvLinkSystem(ds datastore.Datastore) ipld.LinkSystem { 252 lsys := cidlink.DefaultLinkSystem() 253 lsys.StorageReadOpener = func(lctx ipld.LinkContext, lnk ipld.Link) (io.Reader, error) { 254 c := lnk.(cidlink.Link).Cid 255 val, err := ds.Get(lctx.Ctx, datastore.NewKey(c.String())) 256 if err != nil { 257 return nil, err 258 } 259 return bytes.NewBuffer(val), nil 260 } 261 lsys.StorageWriteOpener = func(lctx ipld.LinkContext) (io.Writer, ipld.BlockWriteCommitter, error) { 262 buf := bytes.NewBuffer(nil) 263 return buf, func(lnk ipld.Link) error { 264 c := lnk.(cidlink.Link).Cid 265 return ds.Put(lctx.Ctx, datastore.NewKey(c.String()), buf.Bytes()) 266 }, nil 267 } 268 return lsys 269 } 270 271 func storeRandomIndexAndAd(t *testing.T, eChunkCount int, metadata []byte, prevLink ipld.Link, dstore datastore.Datastore) (ipld.Link, *schema.Advertisement, []multihash.Multihash, peer.ID, crypto.PrivKey) { 272 lsys := mkProvLinkSystem(dstore) 273 274 priv, pubKey, err := p2ptest.RandTestKeyPair(crypto.Ed25519, 256) 275 require.NoError(t, err) 276 277 p, err := peer.IDFromPublicKey(pubKey) 278 require.NoError(t, err) 279 280 ctxID := []byte("test-context-id") 281 if metadata == nil { 282 metadata = []byte("test-metadata") 283 } 284 addrs := []string{"/ip4/127.0.0.1/tcp/9999"} 285 286 adv := &schema.Advertisement{ 287 Provider: p.String(), 288 Addresses: addrs, 289 ContextID: ctxID, 290 Metadata: metadata, 291 PreviousID: prevLink, 292 } 293 var mhs []multihash.Multihash 294 if eChunkCount == 0 { 295 adv.Entries = schema.NoEntries 296 } else { 297 adv.Entries, mhs = newRandomLinkedList(t, lsys, eChunkCount) 298 } 299 300 err = adv.Sign(priv) 301 require.NoError(t, err) 302 303 node, err := adv.ToNode() 304 require.NoError(t, err) 305 306 advLnk, err := lsys.Store(ipld.LinkContext{}, schema.Linkproto, node) 307 require.NoError(t, err) 308 309 return advLnk, adv, mhs, p, priv 310 }