Files
at-container-registry/pkg/hold/pds/layer.go
T
2026-01-04 21:11:32 -06:00

215 lines
6.2 KiB
Go

package pds
import (
"context"
"fmt"
"atcr.io/pkg/atproto"
"atcr.io/pkg/hold/quota"
lexutil "github.com/bluesky-social/indigo/lex/util"
"github.com/bluesky-social/indigo/repo"
)
// CreateLayerRecord creates a new layer record in the hold's PDS
// Returns the rkey and CID of the created record
func (p *HoldPDS) CreateLayerRecord(ctx context.Context, record *atproto.LayerRecord) (string, string, error) {
// Validate record
if record.Type != atproto.LayerCollection {
return "", "", fmt.Errorf("invalid record type: %s", record.Type)
}
if record.Digest == "" {
return "", "", fmt.Errorf("digest is required")
}
if record.Size <= 0 {
return "", "", fmt.Errorf("size must be positive")
}
// Create record with auto-generated TID rkey
rkey, recordCID, err := p.repomgr.CreateRecord(
ctx,
p.uid,
atproto.LayerCollection,
record,
)
if err != nil {
return "", "", fmt.Errorf("failed to create layer record: %w", err)
}
return rkey, recordCID.String(), nil
}
// GetLayerRecord retrieves a specific layer record by rkey
// Note: This is a simplified implementation. For production, you may need to pass the CID
func (p *HoldPDS) GetLayerRecord(ctx context.Context, rkey string) (*atproto.LayerRecord, error) {
// For now, we don't implement this as it's not needed for the manifest post feature
// Full implementation would require querying the carstore with a specific CID
return nil, fmt.Errorf("GetLayerRecord not yet implemented - use via XRPC listRecords instead")
}
// ListLayerRecords lists layer records with pagination
// Returns records, next cursor (empty if no more), and error
// Note: This is a simplified implementation. For production, consider adding filters
// (by repository, user, digest, etc.) and proper pagination
func (p *HoldPDS) ListLayerRecords(ctx context.Context, limit int, cursor string) ([]*atproto.LayerRecord, string, error) {
// For now, return empty list - full implementation would query the carstore
// This would require iterating over records in the collection and filtering
// In practice, layer records are mainly for analytics and Bluesky posts,
// not for runtime queries
return nil, "", fmt.Errorf("ListLayerRecords not yet implemented")
}
// QuotaStats represents storage quota information for a user
type QuotaStats struct {
UserDID string `json:"userDid"`
UniqueBlobs int `json:"uniqueBlobs"`
TotalSize int64 `json:"totalSize"`
Limit *int64 `json:"limit,omitempty"` // nil = unlimited
Berth string `json:"berth,omitempty"` // nautical rank for quota tier
}
// GetQuotaForUser calculates storage quota for a specific user
// It iterates through all layer records, filters by userDid, deduplicates by digest,
// and sums the sizes of unique blobs.
func (p *HoldPDS) GetQuotaForUser(ctx context.Context, userDID string) (*QuotaStats, error) {
if p.recordsIndex == nil {
return nil, fmt.Errorf("records index not available")
}
// Get session for reading record data
session, err := p.carstore.ReadOnlySession(p.uid)
if err != nil {
return nil, fmt.Errorf("failed to create session: %w", err)
}
head, err := p.carstore.GetUserRepoHead(ctx, p.uid)
if err != nil {
return nil, fmt.Errorf("failed to get repo head: %w", err)
}
if !head.Defined() {
// Empty repo - return zero stats
return &QuotaStats{UserDID: userDID}, nil
}
repoHandle, err := repo.OpenRepo(ctx, session, head)
if err != nil {
return nil, fmt.Errorf("failed to open repo: %w", err)
}
// Track unique digests and their sizes
digestSizes := make(map[string]int64)
// Iterate all layer records via the index
cursor := ""
batchSize := 1000 // Process in batches
for {
records, nextCursor, err := p.recordsIndex.ListRecords(atproto.LayerCollection, batchSize, cursor, true)
if err != nil {
return nil, fmt.Errorf("failed to list layer records: %w", err)
}
for _, rec := range records {
// Construct record path and get the record data
recordPath := rec.Collection + "/" + rec.Rkey
_, recBytes, err := repoHandle.GetRecordBytes(ctx, recordPath)
if err != nil {
// Skip records we can't read
continue
}
// Decode the layer record
recordValue, err := lexutil.CborDecodeValue(*recBytes)
if err != nil {
continue
}
layerRecord, ok := recordValue.(*atproto.LayerRecord)
if !ok {
continue
}
// Filter by userDID
if layerRecord.UserDID != userDID {
continue
}
// Deduplicate by digest - keep the size (could be different pushes of same blob)
// Store the size - we only count each unique digest once
if _, exists := digestSizes[layerRecord.Digest]; !exists {
digestSizes[layerRecord.Digest] = layerRecord.Size
}
}
if nextCursor == "" {
break
}
cursor = nextCursor
}
// Calculate totals
var totalSize int64
for _, size := range digestSizes {
totalSize += size
}
return &QuotaStats{
UserDID: userDID,
UniqueBlobs: len(digestSizes),
TotalSize: totalSize,
}, nil
}
// GetQuotaForUserWithBerth calculates quota with berth-aware limits
// It returns the base quota stats plus the berth limit and berth name.
// Captain (owner) always has unlimited quota.
func (p *HoldPDS) GetQuotaForUserWithBerth(ctx context.Context, userDID string, quotaMgr *quota.Manager) (*QuotaStats, error) {
// Get base stats
stats, err := p.GetQuotaForUser(ctx, userDID)
if err != nil {
return nil, err
}
// If quota manager is nil or disabled, return unlimited
if quotaMgr == nil || !quotaMgr.IsEnabled() {
return stats, nil
}
// Check if user is captain (owner) - always unlimited
_, captain, err := p.GetCaptainRecord(ctx)
if err == nil && captain.Owner == userDID {
stats.Berth = "owner"
// Limit remains nil (unlimited)
return stats, nil
}
// Get crew record to find berth
crewBerth := p.getCrewBerth(ctx, userDID)
// Resolve limit from quota manager
stats.Limit = quotaMgr.GetBerthLimit(crewBerth)
stats.Berth = quotaMgr.GetBerthName(crewBerth)
return stats, nil
}
// getCrewBerth returns the berth for a crew member, or empty string if not found
func (p *HoldPDS) getCrewBerth(ctx context.Context, userDID string) string {
crewMembers, err := p.ListCrewMembers(ctx)
if err != nil {
return ""
}
for _, member := range crewMembers {
if member.Record.Member == userDID {
return member.Record.Berth
}
}
return ""
}