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{} } // Build approvals from item_approvals table. var approvals []kc.ApprovalEntry dbApprovals, err := s.approvals.ListByItemWithSignatures(ctx, item.ID) if err != nil { s.logger.Warn().Err(err).Str("part_number", item.PartNumber).Msg("kc: failed to query approvals for packing") } else { approvals = make([]kc.ApprovalEntry, len(dbApprovals)) for i, a := range dbApprovals { sigs := make([]kc.SignatureEntry, len(a.Signatures)) for j, sig := range a.Signatures { var signedAt string if sig.SignedAt != nil { signedAt = sig.SignedAt.UTC().Format("2006-01-02T15:04:05Z") } var comment string if sig.Comment != nil { comment = *sig.Comment } sigs[j] = kc.SignatureEntry{ Username: sig.Username, Role: sig.Role, Status: sig.Status, SignedAt: signedAt, Comment: comment, } } var ecoNumber string if a.ECONumber != nil { ecoNumber = *a.ECONumber } var updatedBy string if a.UpdatedBy != nil { updatedBy = *a.UpdatedBy } approvals[i] = kc.ApprovalEntry{ ID: a.ID, WorkflowName: a.WorkflowName, ECONumber: ecoNumber, State: a.State, UpdatedAt: a.UpdatedAt.UTC().Format("2006-01-02T15:04:05Z"), UpdatedBy: updatedBy, Signatures: sigs, } } } input := &kc.PackInput{ Manifest: manifest, Metadata: metadata, History: history, Dependencies: deps, Approvals: approvals, } 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 }