package api import ( "context" "crypto/sha256" "encoding/hex" "fmt" "time" "github.com/kindredsystems/silo/internal/db" "github.com/kindredsystems/silo/internal/kc" ) // packKCFile gathers DB state and repacks silo/ entries in a .kc file. func (s *Server) packKCFile(ctx context.Context, data []byte, item *db.Item, rev *db.Revision, meta *db.ItemMetadata) ([]byte, error) { manifest := &kc.Manifest{ UUID: item.ID, KCVersion: derefStr(meta.KCVersion, "1.0"), RevisionHash: derefStr(meta.RevisionHash, ""), SiloInstance: derefStr(meta.SiloInstance, ""), } metadata := &kc.Metadata{ SchemaName: derefStr(meta.SchemaName, ""), Tags: meta.Tags, LifecycleState: meta.LifecycleState, Fields: meta.Fields, } // Build history from last 20 revisions. revisions, err := s.items.GetRevisions(ctx, item.ID) if err != nil { return nil, fmt.Errorf("getting revisions: %w", err) } limit := 20 if len(revisions) < limit { limit = len(revisions) } history := make([]kc.HistoryEntry, limit) for i, r := range revisions[:limit] { labels := r.Labels if labels == nil { labels = []string{} } history[i] = kc.HistoryEntry{ RevisionNumber: r.RevisionNumber, CreatedAt: r.CreatedAt.UTC().Format(time.RFC3339), CreatedBy: r.CreatedBy, Comment: r.Comment, Status: r.Status, Labels: labels, } } // Build dependencies from item_dependencies table. var deps []kc.Dependency dbDeps, err := s.deps.ListByItem(ctx, item.ID) if err != nil { s.logger.Warn().Err(err).Str("part_number", item.PartNumber).Msg("kc: failed to query dependencies for packing") } else { deps = make([]kc.Dependency, len(dbDeps)) for i, d := range dbDeps { deps[i] = kc.Dependency{ UUID: d.ChildUUID, PartNumber: derefStr(d.ChildPartNumber, ""), Revision: derefInt(d.ChildRevision, 0), Quantity: derefFloat(d.Quantity, 0), Label: derefStr(d.Label, ""), Relationship: d.Relationship, } } } if deps == nil { deps = []kc.Dependency{} } input := &kc.PackInput{ Manifest: manifest, Metadata: metadata, History: history, Dependencies: deps, } return kc.Pack(data, input) } // computeETag generates a quoted ETag from the revision number and metadata freshness. func computeETag(rev *db.Revision, meta *db.ItemMetadata) string { var ts int64 if meta != nil { ts = meta.UpdatedAt.UnixNano() } else { ts = rev.CreatedAt.UnixNano() } raw := fmt.Sprintf("%d:%d", rev.RevisionNumber, ts) h := sha256.Sum256([]byte(raw)) return `"` + hex.EncodeToString(h[:8]) + `"` } // canSkipRepack returns true if the stored blob already has up-to-date silo/ data. func canSkipRepack(rev *db.Revision, meta *db.ItemMetadata) bool { if meta == nil { return true // no metadata row = plain .fcstd } if meta.RevisionHash != nil && rev.FileChecksum != nil && *meta.RevisionHash == *rev.FileChecksum && meta.UpdatedAt.Before(rev.CreatedAt) { return true } return false } // derefStr returns the value of a *string pointer, or fallback if nil. func derefStr(p *string, fallback string) string { if p != nil { return *p } return fallback } // derefInt returns the value of a *int pointer, or fallback if nil. func derefInt(p *int, fallback int) int { if p != nil { return *p } return fallback } // derefFloat returns the value of a *float64 pointer, or fallback if nil. func derefFloat(p *float64, fallback float64) float64 { if p != nil { return *p } return fallback }