package blob import ( "crypto/sha256" "errors" "io" "os" ) // Chunk represents a range of bytes in a blob. type Chunk struct { Start int64 End int64 } // Size returns end minus start plus one. func (c Chunk) Size() int64 { return c.End - c.Start + 1 } // Chunker writes to a blob in chunks. // Its zero value is invalid. Use [DiskCache.Chunked] to create a new Chunker. type Chunker struct { digest Digest size int64 f *os.File // nil means pre-validated } // Chunked returns a new Chunker, ready for use storing a blob of the given // size in chunks. // // Use [Chunker.Put] to write data to the blob at specific offsets. func (c *DiskCache) Chunked(d Digest, size int64) (*Chunker, error) { name := c.GetFile(d) info, err := os.Stat(name) if err == nil && info.Size() == size { return &Chunker{}, nil } f, err := os.OpenFile(name, os.O_CREATE|os.O_WRONLY, 0o666) if err != nil { return nil, err } return &Chunker{digest: d, size: size, f: f}, nil } // Put copies chunk.Size() bytes from r to the blob at the given offset, // merging the data with the existing blob. It returns an error if any. As a // special case, if r has less than chunk.Size() bytes, Put returns // io.ErrUnexpectedEOF. func (c *Chunker) Put(chunk Chunk, d Digest, r io.Reader) error { if c.f == nil { return nil } cw := &checkWriter{ d: d, size: chunk.Size(), h: sha256.New(), f: c.f, w: io.NewOffsetWriter(c.f, chunk.Start), } _, err := io.CopyN(cw, r, chunk.Size()) if err != nil && errors.Is(err, io.EOF) { return io.ErrUnexpectedEOF } return err } // Close closes the underlying file. func (c *Chunker) Close() error { return c.f.Close() }