~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
88 lines · 2.5 KB Raw History
package files

import (
	"bytes"
	"cmp"
	"context"
	"crypto/sha256"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"strings"

	"github.com/bluesky-social/indigo/atproto/syntax"
	"github.com/ipfs/go-cid"
	"github.com/multiformats/go-multihash"

	atpds "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/pds"
)

var _ atpds.BlobStore = (*FileBlobStore)(nil)

type FileBlobStore struct {
	dir string
}

func NewFileBlobStore(dir string) (*FileBlobStore, error) {
	dir = strings.TrimSpace(dir)
	if dir == "" {
		return nil, errors.New("pds: the file blob store needs a directory")
	}
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return nil, fmt.Errorf("pds: creating the blob directory: %w", err)
	}
	return &FileBlobStore{dir: dir}, nil
}

func (s *FileBlobStore) PutBlob(_ context.Context, _ syntax.DID, mimeType string, data []byte) (*atpds.BlobRef, error) {
	mimeType = strings.TrimSpace(mimeType)
	mimeType = cmp.Or(mimeType, "application/octet-stream")
	if strings.ContainsAny(mimeType, "\r\n") {
		return nil, errors.New("pds: the blob mime type has a newline")
	}
	sum := sha256.Sum256(data)
	mh, err := multihash.Encode(sum[:], multihash.SHA2_256)
	if err != nil {
		return nil, fmt.Errorf("pds: encoding the blob hash: %w", err)
	}
	c := cid.NewCidV1(cid.Raw, mh)
	payload := make([]byte, 0, len(mimeType)+1+len(data))
	payload = append(payload, mimeType...)
	payload = append(payload, '\n')
	payload = append(payload, data...)
	if err := writePrivateFile(s.path(c), payload); err != nil {
		return nil, fmt.Errorf("pds: writing the blob: %w", err)
	}
	return &atpds.BlobRef{CID: c, MimeType: mimeType, Size: int64(len(data))}, nil
}

func (s *FileBlobStore) GetBlob(_ context.Context, _ syntax.DID, c cid.Cid) ([]byte, string, error) {
	payload, err := os.ReadFile(s.path(c))
	if err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			return nil, "", atpds.ErrBlobNotFound
		}
		return nil, "", fmt.Errorf("pds: reading the blob: %w", err)
	}
	before, after, ok := bytes.Cut(payload, []byte{'\n'})
	if !ok {
		return nil, "", fmt.Errorf("pds: the blob file is invalid")
	}
	mimeType := string(before)
	data := after
	sum := sha256.Sum256(data)
	mh, err := multihash.Encode(sum[:], multihash.SHA2_256)
	if err != nil {
		return nil, "", fmt.Errorf("pds: encoding the blob hash: %w", err)
	}
	if !cid.NewCidV1(cid.Raw, mh).Equals(c) {
		return nil, "", fmt.Errorf("pds: the blob content does not match its cid")
	}
	return data, mimeType, nil
}

func (s *FileBlobStore) path(c cid.Cid) string {
	return filepath.Join(s.dir, c.String()+".blob")
}