1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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")
}