forked from TrueCloudLab/frostfs-node
209 lines
5.6 KiB
Go
209 lines
5.6 KiB
Go
package blobtree
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
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
|
|
|
|
dataExtension = ".data"
|
|
)
|
|
|
|
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(b.getSystemPath(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 {
|
|
p = b.getSystemPath(p)
|
|
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) getSystemPath(path string) string {
|
|
return filepath.Join(b.cfg.rootPath, path)
|
|
}
|
|
|
|
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)+dataExtension)
|
|
}
|
|
|
|
func (b *BlobTree) parsePath(path string) (string, uint64, error) {
|
|
dir := filepath.Dir(path)
|
|
fileName := strings.TrimSuffix(filepath.Base(path), dataExtension)
|
|
idx, err := strconv.ParseUint(fileName, 16, 64)
|
|
if err != nil {
|
|
return "", 0, fmt.Errorf("failed to parse blobtree path: %w", err)
|
|
}
|
|
return dir, idx, nil
|
|
}
|