Enhance scratchbox storage metadata management
CI / Go Tests (push) Successful in 14s
CI / Build (push) Successful in 8s
Format / gofmt (push) Failing after 4s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 14s
Release Artifacts / Build and release Docker image (push) Successful in 24s
CI / Go Tests (push) Successful in 14s
CI / Build (push) Successful in 8s
Format / gofmt (push) Failing after 4s
Release Artifacts / Validate release tag (push) Successful in 2s
Release Artifacts / Build and release executables (push) Successful in 14s
Release Artifacts / Build and release Docker image (push) Successful in 24s
- Updated `README.md` to clarify the distinction between public scratch IDs and internal blob IDs. - Modified `filesystem.go` to introduce a new `BlobID` field in the `Metadata` struct, ensuring the separation of public and internal identifiers. - Implemented functions to generate and reserve unique public IDs for scratch entries, improving metadata integrity. - Adjusted tests in `filesystem_test.go` to validate the new ID structure and ensure public IDs do not match blob hashes.
This commit is contained in:
@@ -294,4 +294,4 @@ Scratchbox storage is designed for a single process instance per `storage.data_d
|
||||
- Do not run multiple scratchbox processes against the same data directory.
|
||||
- Metadata/index writes are process-local and are not coordinated with cross-process locking.
|
||||
- Scratch payload files and metadata index are encrypted at rest.
|
||||
- Scratch IDs are derived from the SHA-256 of the stored blob (encrypted+gzip payload on disk).
|
||||
- Public scratch IDs are short random aliases; stored blobs still use SHA-256-derived internal blob IDs on disk.
|
||||
|
||||
@@ -21,9 +21,12 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
indexFilename = "metadata"
|
||||
filesDirName = "scratches"
|
||||
encryptedChunkSize = 64 * 1024
|
||||
indexFilename = "metadata"
|
||||
filesDirName = "scratches"
|
||||
encryptedChunkSize = 64 * 1024
|
||||
publicIDLength = 12
|
||||
publicIDAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
publicIDMaxAttempts = 8
|
||||
)
|
||||
|
||||
var ErrNotFound = errors.New("scratch not found")
|
||||
@@ -33,6 +36,7 @@ var encryptedIndexMagic = [4]byte{'S', 'M', 'D', '1'}
|
||||
|
||||
type Metadata struct {
|
||||
ID string `json:"id"`
|
||||
BlobID string `json:"blob_id,omitempty"`
|
||||
FilePath string `json:"file_path"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
@@ -146,8 +150,8 @@ func (s *FilesystemStore) CreateWithOriginalName(ctx context.Context, body io.Re
|
||||
return Metadata{}, fmt.Errorf("close temp file: %w", closeErr)
|
||||
}
|
||||
|
||||
id := hex.EncodeToString(blobHash.Sum(nil))
|
||||
finalPath := filepath.Join(s.filesDir, id)
|
||||
blobID := hex.EncodeToString(blobHash.Sum(nil))
|
||||
finalPath := filepath.Join(s.filesDir, blobID)
|
||||
if _, statErr := os.Stat(finalPath); statErr == nil {
|
||||
_ = os.Remove(tempFile.Name())
|
||||
return Metadata{}, errors.New("scratch id collision for content hash")
|
||||
@@ -161,8 +165,17 @@ func (s *FilesystemStore) CreateWithOriginalName(ctx context.Context, body io.Re
|
||||
}
|
||||
|
||||
now := time.Now().UTC()
|
||||
|
||||
s.mu.Lock()
|
||||
publicID, err := reservePublicID(s.metadata)
|
||||
if err != nil {
|
||||
s.mu.Unlock()
|
||||
_ = os.Remove(finalPath)
|
||||
return Metadata{}, err
|
||||
}
|
||||
meta := Metadata{
|
||||
ID: id,
|
||||
ID: publicID,
|
||||
BlobID: blobID,
|
||||
FilePath: finalPath,
|
||||
CreatedAt: now,
|
||||
ExpiresAt: now.Add(ttl),
|
||||
@@ -170,16 +183,9 @@ func (s *FilesystemStore) CreateWithOriginalName(ctx context.Context, body io.Re
|
||||
OriginalName: normalizeOriginalName(originalName),
|
||||
Size: size,
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
if _, exists := s.metadata[id]; exists {
|
||||
s.mu.Unlock()
|
||||
_ = os.Remove(finalPath)
|
||||
return Metadata{}, errors.New("scratch id collision in metadata index")
|
||||
}
|
||||
s.metadata[id] = meta
|
||||
s.metadata[publicID] = meta
|
||||
if err := s.persistLocked(); err != nil {
|
||||
delete(s.metadata, id)
|
||||
delete(s.metadata, publicID)
|
||||
s.mu.Unlock()
|
||||
_ = os.Remove(finalPath)
|
||||
return Metadata{}, err
|
||||
@@ -314,6 +320,15 @@ func (s *FilesystemStore) loadIndex() error {
|
||||
if doc.Scratches == nil {
|
||||
doc.Scratches = map[string]Metadata{}
|
||||
}
|
||||
for key, meta := range doc.Scratches {
|
||||
if strings.TrimSpace(meta.ID) == "" {
|
||||
meta.ID = key
|
||||
}
|
||||
if strings.TrimSpace(meta.BlobID) == "" {
|
||||
meta.BlobID = filepath.Base(meta.FilePath)
|
||||
}
|
||||
doc.Scratches[key] = meta
|
||||
}
|
||||
s.metadata = doc.Scratches
|
||||
if ttlRaw := strings.TrimSpace(doc.DefaultTTL); ttlRaw != "" {
|
||||
ttl, parseErr := time.ParseDuration(ttlRaw)
|
||||
@@ -582,6 +597,31 @@ func equalBytes(a, b []byte) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func reservePublicID(existing map[string]Metadata) (string, error) {
|
||||
for i := 0; i < publicIDMaxAttempts; i++ {
|
||||
id, err := generatePublicID()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, exists := existing[id]; !exists {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
return "", errors.New("unable to reserve unique public id")
|
||||
}
|
||||
|
||||
func generatePublicID() (string, error) {
|
||||
buf := make([]byte, publicIDLength)
|
||||
if _, err := rand.Read(buf); err != nil {
|
||||
return "", fmt.Errorf("generate public id bytes: %w", err)
|
||||
}
|
||||
alphabetLen := byte(len(publicIDAlphabet))
|
||||
for i := range buf {
|
||||
buf[i] = publicIDAlphabet[int(buf[i]%alphabetLen)]
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func encryptIndexPayload(plaintext []byte, aead cipher.AEAD) ([]byte, error) {
|
||||
nonce := make([]byte, aead.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
|
||||
@@ -123,9 +123,12 @@ func TestCreateEncryptedStoreDoesNotExposeGzipHeaderOnDisk(t *testing.T) {
|
||||
t.Fatalf("ReadFile(encrypted) error = %v", err)
|
||||
}
|
||||
sum := sha256.Sum256(filePayload)
|
||||
wantID := hex.EncodeToString(sum[:])
|
||||
if meta.ID != wantID {
|
||||
t.Fatalf("meta.ID = %q, want SHA-256 of encrypted blob %q", meta.ID, wantID)
|
||||
wantBlobID := hex.EncodeToString(sum[:])
|
||||
if meta.BlobID != wantBlobID {
|
||||
t.Fatalf("meta.BlobID = %q, want SHA-256 of encrypted blob %q", meta.BlobID, wantBlobID)
|
||||
}
|
||||
if meta.ID == wantBlobID {
|
||||
t.Fatalf("meta.ID should be a short public id, got blob hash id %q", meta.ID)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user