89 lines
2.9 KiB
Go
89 lines
2.9 KiB
Go
// Package cas is the content-addressed blob store. Every file any deployment
|
|
// contains is stored once, named by the SHA-256 of its bytes, and shared by
|
|
// every deployment and every project that references that content.
|
|
//
|
|
// Sharing is what makes redeploying a mostly-unchanged site nearly free, and it
|
|
// is only safe because the store never takes a caller's word for a digest: see
|
|
// Store.Put.
|
|
package cas
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
)
|
|
|
|
const (
|
|
// Size is a digest's length in bytes.
|
|
Size = sha256.Size
|
|
// HexLen is a digest's length in its hex form.
|
|
HexLen = 2 * Size
|
|
)
|
|
|
|
// Digest is the SHA-256 of a blob's contents.
|
|
//
|
|
// An array rather than a slice, so it can be a map key, compared with ==, and
|
|
// copied out of a database buffer instead of aliasing one — database/sql reuses
|
|
// the byte slices it scans into.
|
|
type Digest [Size]byte
|
|
|
|
// ErrBadDigest rejects anything that is not a digest in the canonical form.
|
|
var ErrBadDigest = errors.New("cas: not a 64-character lowercase hex sha-256")
|
|
|
|
// ParseDigest decodes the hex form used on the wire and in URLs.
|
|
//
|
|
// Strict about case rather than normalising, because a digest is simultaneously
|
|
// a primary key in the blobs table and a component of a filesystem path.
|
|
// Accepting two spellings of one value would mean two rows, two files, and
|
|
// deduplication that quietly stops deduplicating.
|
|
func ParseDigest(s string) (Digest, error) {
|
|
var d Digest
|
|
if len(s) != HexLen {
|
|
return d, ErrBadDigest
|
|
}
|
|
for i := 0; i < len(s); i++ {
|
|
// encoding/hex accepts uppercase; this loop is what makes lowercase the
|
|
// only accepted spelling.
|
|
c := s[i]
|
|
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
|
continue
|
|
}
|
|
return d, ErrBadDigest
|
|
}
|
|
if _, err := hex.Decode(d[:], []byte(s)); err != nil {
|
|
return Digest{}, ErrBadDigest
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
// FromBytes converts the raw 32 bytes stored in the database. It copies, so the
|
|
// result does not alias the scan buffer it came from.
|
|
func FromBytes(b []byte) (Digest, error) {
|
|
var d Digest
|
|
if len(b) != Size {
|
|
return d, fmt.Errorf("cas: digest is %d bytes, want %d", len(b), Size)
|
|
}
|
|
copy(d[:], b)
|
|
return d, nil
|
|
}
|
|
|
|
// String is the canonical hex form.
|
|
func (d Digest) String() string { return hex.EncodeToString(d[:]) }
|
|
|
|
// Bytes is the raw form stored in the database. The slice belongs to the
|
|
// caller's copy of the digest, so writing to it cannot affect anything else.
|
|
func (d Digest) Bytes() []byte { return d[:] }
|
|
|
|
// Rel is the blob's path inside the store, sharded two levels deep:
|
|
// "ab/cd/abcd…". Two hex characters per level gives 65,536 leaf directories, so
|
|
// even a store with millions of blobs keeps every directory small enough that
|
|
// readdir and lookup stay fast on ext4 and xfs alike.
|
|
func (d Digest) Rel() string {
|
|
s := d.String()
|
|
return s[0:2] + "/" + s[2:4] + "/" + s
|
|
}
|
|
|
|
// Sum digests b.
|
|
func Sum(b []byte) Digest { return sha256.Sum256(b) }
|