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  }