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 }