frostfs-node/pkg/local_object_storage/blobstor/blobtree/content.go
Dmitrii Stepanov 82a30c0775 [#645] blobstor: Add simple blobtree impl
Signed-off-by: Dmitrii Stepanov <d.stepanov@yadro.com>
2023-11-29 19:34:22 +03:00

191 lines
5.1 KiB
Go

package blobtree
import (
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
)
const (
defaultVersion = 0
sizeOfVersion = 1
sizeOfCount = 8
sizeOfDataLength = 8
sizeOfContainerID = sha256.Size
sizeOfObjectID = sha256.Size
)
var (
errFileToSmall = errors.New("invalid file content: not enough bytes to read count of records")
errInvalidFileContentVersion = errors.New("invalid file content: not enough bytes to read record version")
errInvalidFileContentContainerID = errors.New("invalid file content: not enough bytes to read container ID")
errInvalidFileContentObjectID = errors.New("invalid file content: not enough bytes to read object ID")
errInvalidFileContentLength = errors.New("invalid file content: not enough bytes to read data length")
errInvalidFileContentData = errors.New("invalid file content: not enough bytes to read data")
)
type objectData struct {
Version byte
Address oid.Address
Data []byte
}
func (b *BlobTree) readFileContent(path string) ([]objectData, error) {
rawData, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return []objectData{}, nil
}
return nil, err
}
return b.unmarshalSlice(rawData)
}
func (b *BlobTree) unmarshalSlice(data []byte) ([]objectData, error) {
if len(data) < sizeOfCount {
return nil, errFileToSmall
}
count := binary.LittleEndian.Uint64(data[:8])
result := make([]objectData, 0, count)
data = data[sizeOfCount:]
var idx uint64
for idx = 0; idx < count; idx++ {
record, read, err := b.unmarshalRecord(data)
if err != nil {
return nil, err
}
result = append(result, record)
data = data[read:]
}
return result, nil
}
func (b *BlobTree) unmarshalRecord(data []byte) (objectData, uint64, error) {
if len(data) < sizeOfVersion {
return objectData{}, 0, errInvalidFileContentVersion
}
var result objectData
var read uint64
result.Version = data[0]
if result.Version != defaultVersion {
return objectData{}, 0, fmt.Errorf("invalid file content: unknown version %d", result.Version)
}
read += sizeOfVersion
if len(data[read:]) < sizeOfContainerID {
return objectData{}, 0, errInvalidFileContentContainerID
}
var contID cid.ID
if err := contID.Decode(data[read : read+sizeOfContainerID]); err != nil {
return objectData{}, 0, fmt.Errorf("invalid file content: failed to read container ID: %w", err)
}
read += sizeOfContainerID
if len(data[read:]) < sizeOfObjectID {
return objectData{}, 0, errInvalidFileContentObjectID
}
var objID oid.ID
if err := objID.Decode(data[read : read+sizeOfObjectID]); err != nil {
return objectData{}, 0, fmt.Errorf("invalid file content: failed to read object ID: %w", err)
}
read += sizeOfObjectID
result.Address.SetContainer(contID)
result.Address.SetObject(objID)
if len(data[read:]) < sizeOfDataLength {
return objectData{}, 0, errInvalidFileContentLength
}
dataLength := binary.LittleEndian.Uint64(data[read : read+sizeOfDataLength])
read += sizeOfDataLength
if uint64(len(data[read:])) < dataLength {
return objectData{}, 0, errInvalidFileContentData
}
result.Data = make([]byte, dataLength)
copy(result.Data, data[read:read+dataLength])
read += dataLength
return result, read, nil
}
func (b *BlobTree) saveContentToFile(records []objectData, path string) (uint64, error) {
data, err := b.marshalSlice(records)
if err != nil {
return 0, err
}
return uint64(len(data)), b.writeFile(path, data)
}
func (b *BlobTree) writeFile(p string, data []byte) error {
f, err := os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_EXCL|os.O_SYNC, b.cfg.permissions)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
func (b *BlobTree) marshalSlice(records []objectData) ([]byte, error) {
buf := make([]byte, b.estimateSize(records))
result := buf
binary.LittleEndian.PutUint64(buf, uint64(len(records)))
buf = buf[sizeOfCount:]
for _, record := range records {
written := b.marshalRecord(record, buf)
buf = buf[written:]
}
return result, nil
}
func (b *BlobTree) marshalRecord(record objectData, dst []byte) uint64 {
var written uint64
dst[0] = record.Version
dst = dst[sizeOfVersion:]
written += sizeOfVersion
record.Address.Container().Encode(dst)
dst = dst[sizeOfContainerID:]
written += sizeOfContainerID
record.Address.Object().Encode(dst)
dst = dst[sizeOfObjectID:]
written += sizeOfObjectID
binary.LittleEndian.PutUint64(dst, uint64(len(record.Data)))
dst = dst[sizeOfDataLength:]
written += sizeOfDataLength
copy(dst, record.Data)
written += uint64(len(record.Data))
return written
}
func (b *BlobTree) estimateSize(records []objectData) uint64 {
var result uint64
result += sizeOfCount
for _, record := range records {
result += (sizeOfVersion + sizeOfContainerID + sizeOfObjectID + sizeOfDataLength)
result += uint64(len(record.Data))
}
return result
}
func (b *BlobTree) getFilePath(dir string, idx uint64) string {
return filepath.Join(dir, strconv.FormatUint(idx, 16))
}