github.com/bazelbuild/remote-apis-sdks@v0.0.0-20240425170053-8a36686a6350/go/pkg/client/bytestream.go (about)

     1  package client
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  
    10  	log "github.com/golang/glog"
    11  	"github.com/pkg/errors"
    12  	bspb "google.golang.org/genproto/googleapis/bytestream"
    13  
    14  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/chunker"
    15  	"github.com/bazelbuild/remote-apis-sdks/go/pkg/uploadinfo"
    16  )
    17  
    18  // WriteBytes uploads a byte slice.
    19  func (c *Client) WriteBytes(ctx context.Context, name string, data []byte) error {
    20  	ue := uploadinfo.EntryFromBlob(data)
    21  	ch, err := chunker.New(ue, false, int(c.ChunkMaxSize))
    22  	if err != nil {
    23  		return err
    24  	}
    25  	_, err = c.writeChunked(ctx, name, ch, false, 0)
    26  	return err
    27  }
    28  
    29  // WriteBytesAtRemoteOffset uploads a byte slice with a given resource name to the CAS
    30  // at an arbitrary offset but retries still resend from the initial Offset. As of now(2023-02-08),
    31  // ByteStream.WriteRequest.FinishWrite and an arbitrary offset are supported for uploads with LogStream
    32  // resource name. If doNotFinalize is set to true, ByteStream.WriteRequest.FinishWrite will be set to false.
    33  func (c *Client) WriteBytesAtRemoteOffset(ctx context.Context, name string, data []byte, doNotFinalize bool, initialOffset int64) (int64, error) {
    34  	ue := uploadinfo.EntryFromBlob(data)
    35  	ch, err := chunker.New(ue, false, int(c.ChunkMaxSize))
    36  	if err != nil {
    37  		return 0, errors.Wrap(err, "failed to create a chunk")
    38  	}
    39  	writtenBytes, err := c.writeChunked(ctx, name, ch, doNotFinalize, initialOffset)
    40  	if err != nil {
    41  		return 0, err
    42  	}
    43  	return writtenBytes, nil
    44  }
    45  
    46  // writeChunked uploads chunked data with a given resource name to the CAS.
    47  func (c *Client) writeChunked(ctx context.Context, name string, ch *chunker.Chunker, doNotFinalize bool, initialOffset int64) (int64, error) {
    48  	var totalBytes int64
    49  	closure := func() error {
    50  		// Retry by starting the stream from the beginning.
    51  		if err := ch.Reset(); err != nil {
    52  			return errors.Wrap(err, "failed to Reset")
    53  		}
    54  		totalBytes = int64(0)
    55  		// TODO(olaola): implement resumable uploads. initialOffset passed in allows to
    56  		// start writing data at an arbitrary offset, but retries still restart from initialOffset.
    57  
    58  		stream, err := c.Write(ctx)
    59  		if err != nil {
    60  			return err
    61  		}
    62  		for ch.HasNext() {
    63  			req := &bspb.WriteRequest{ResourceName: name}
    64  			chunk, err := ch.Next()
    65  			if err != nil {
    66  				return err
    67  			}
    68  			req.WriteOffset = chunk.Offset + initialOffset
    69  			req.Data = chunk.Data
    70  
    71  			if !ch.HasNext() && !doNotFinalize {
    72  				req.FinishWrite = true
    73  			}
    74  			err = c.CallWithTimeout(ctx, "Write", func(_ context.Context) error { return stream.Send(req) })
    75  			if err == io.EOF {
    76  				break
    77  			}
    78  			if err != nil {
    79  				return err
    80  			}
    81  			totalBytes += int64(len(req.Data))
    82  		}
    83  		if _, err := stream.CloseAndRecv(); err != nil {
    84  			return err
    85  		}
    86  		return nil
    87  	}
    88  	err := c.Retrier.Do(ctx, closure)
    89  	return totalBytes, err
    90  }
    91  
    92  // ReadBytes fetches a resource's contents into a byte slice.
    93  //
    94  // ReadBytes panics with ErrTooLarge if an attempt is made to read a resource with contents too
    95  // large to fit into a byte array.
    96  func (c *Client) ReadBytes(ctx context.Context, name string) ([]byte, error) {
    97  	buf := &bytes.Buffer{}
    98  	_, err := c.readStreamedRetried(ctx, name, 0, 0, buf)
    99  	return buf.Bytes(), err
   100  }
   101  
   102  // ReadResourceTo writes a resource's contents to a Writer.
   103  func (c *Client) ReadResourceTo(ctx context.Context, name string, w io.Writer) (int64, error) {
   104  	return c.readStreamedRetried(ctx, name, 0, 0, w)
   105  }
   106  
   107  // ReadResourceToFile fetches a resource's contents, saving it into a file.
   108  //
   109  // The provided resource name must be a child resource of this client's instance,
   110  // e.g. '/blobs/abc-123/45' (NOT 'projects/foo/bar/baz').
   111  //
   112  // The number of bytes read is returned.
   113  func (c *Client) ReadResourceToFile(ctx context.Context, name, fpath string) (int64, error) {
   114  	rname, err := c.ResourceName(name)
   115  	if err != nil {
   116  		return 0, err
   117  	}
   118  	return c.readToFile(ctx, rname, fpath)
   119  }
   120  
   121  func (c *Client) readToFile(ctx context.Context, name string, fpath string) (int64, error) {
   122  	f, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, c.RegularMode)
   123  	if err != nil {
   124  		return 0, err
   125  	}
   126  	defer f.Close()
   127  	return c.readStreamedRetried(ctx, name, 0, 0, f)
   128  }
   129  
   130  // readStreamed reads from a bytestream and copies the result to the provided Writer, starting
   131  // offset bytes into the stream and reading at most limit bytes (or no limit if limit==0). The
   132  // offset must be non-negative, and an error may be returned if the offset is past the end of the
   133  // stream. The limit must be non-negative, although offset+limit may exceed the length of the
   134  // stream.
   135  func (c *Client) readStreamed(ctx context.Context, name string, offset, limit int64, w io.Writer) (int64, error) {
   136  	stream, err := c.Read(ctx, &bspb.ReadRequest{
   137  		ResourceName: name,
   138  		ReadOffset:   offset,
   139  		ReadLimit:    limit,
   140  	})
   141  	if err != nil {
   142  		return 0, err
   143  	}
   144  
   145  	var n int64
   146  	for {
   147  		var resp *bspb.ReadResponse
   148  		err := c.CallWithTimeout(ctx, "Read", func(_ context.Context) error {
   149  			r, err := stream.Recv()
   150  			resp = r
   151  			return err
   152  		})
   153  		if err == io.EOF {
   154  			break
   155  		}
   156  		if err != nil {
   157  			return 0, err
   158  		}
   159  		log.V(3).Infof("Read: resource:%s offset:%d len(data):%d", name, offset, len(resp.Data))
   160  		nm, err := w.Write(resp.Data)
   161  		if err != nil {
   162  			// Wrapping the error to ensure it may never get retried.
   163  			return int64(nm), fmt.Errorf("failed to write to output stream: %v", err)
   164  		}
   165  		sz := len(resp.Data)
   166  		if nm != sz {
   167  			return int64(nm), fmt.Errorf("received %d bytes but could only write %d", sz, nm)
   168  		}
   169  		n += int64(sz)
   170  		if limit > 0 {
   171  			limit -= int64(sz)
   172  			if limit <= 0 {
   173  				break
   174  			}
   175  		}
   176  	}
   177  	return n, nil
   178  }
   179  
   180  func (c *Client) readStreamedRetried(ctx context.Context, name string, offset, limit int64, w io.Writer) (int64, error) {
   181  	var n int64
   182  	closure := func() error {
   183  		m, err := c.readStreamed(ctx, name, offset+n, limit, w)
   184  		n += m
   185  		return err
   186  	}
   187  	return n, c.Retrier.Do(ctx, closure)
   188  }