package kc import ( "archive/zip" "bytes" "encoding/json" "fmt" "io" "strings" ) // HasSiloDir opens a ZIP archive and returns true if any entry starts with "silo/". // This is a lightweight check used to short-circuit before gathering DB data. func HasSiloDir(data []byte) (bool, error) { r, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) if err != nil { return false, fmt.Errorf("kc: open zip: %w", err) } for _, f := range r.File { if f.Name == "silo/" || strings.HasPrefix(f.Name, "silo/") { return true, nil } } return false, nil } // Pack takes original ZIP file bytes and a PackInput, and returns new ZIP bytes // with all silo/ entries replaced by the data from input. Non-silo entries // (FreeCAD Document.xml, thumbnails, etc.) are copied verbatim with their // original compression method and timestamps preserved. // // If the original ZIP contains no silo/ directory, the original bytes are // returned unchanged (plain .fcstd pass-through). func Pack(original []byte, input *PackInput) ([]byte, error) { r, err := zip.NewReader(bytes.NewReader(original), int64(len(original))) if err != nil { return nil, fmt.Errorf("kc: open zip: %w", err) } // Partition entries into silo/ vs non-silo. hasSilo := false for _, f := range r.File { if f.Name == "silo/" || strings.HasPrefix(f.Name, "silo/") { hasSilo = true break } } if !hasSilo { return original, nil // plain .fcstd, no repacking needed } var buf bytes.Buffer zw := zip.NewWriter(&buf) // Copy all non-silo entries verbatim. for _, f := range r.File { if f.Name == "silo/" || strings.HasPrefix(f.Name, "silo/") { continue } if err := copyZipEntry(zw, f); err != nil { return nil, fmt.Errorf("kc: copying entry %s: %w", f.Name, err) } } // Write new silo/ entries from PackInput. if input.Manifest != nil { if err := writeJSONEntry(zw, "silo/manifest.json", input.Manifest); err != nil { return nil, fmt.Errorf("kc: writing manifest.json: %w", err) } } if input.Metadata != nil { if err := writeJSONEntry(zw, "silo/metadata.json", input.Metadata); err != nil { return nil, fmt.Errorf("kc: writing metadata.json: %w", err) } } if input.History != nil { if err := writeJSONEntry(zw, "silo/history.json", input.History); err != nil { return nil, fmt.Errorf("kc: writing history.json: %w", err) } } if input.Dependencies != nil { if err := writeJSONEntry(zw, "silo/dependencies.json", input.Dependencies); err != nil { return nil, fmt.Errorf("kc: writing dependencies.json: %w", err) } } if input.Approvals != nil { if err := writeJSONEntry(zw, "silo/approvals.json", input.Approvals); err != nil { return nil, fmt.Errorf("kc: writing approvals.json: %w", err) } } if err := zw.Close(); err != nil { return nil, fmt.Errorf("kc: closing zip writer: %w", err) } return buf.Bytes(), nil } // copyZipEntry copies a single entry from the original ZIP to the new writer, // preserving the file header (compression method, timestamps, etc.). func copyZipEntry(zw *zip.Writer, f *zip.File) error { header := f.FileHeader w, err := zw.CreateHeader(&header) if err != nil { return err } rc, err := f.Open() if err != nil { return err } defer rc.Close() _, err = io.Copy(w, rc) return err } // writeJSONEntry writes a new silo/ entry as JSON with Deflate compression. func writeJSONEntry(zw *zip.Writer, name string, v any) error { data, err := json.MarshalIndent(v, "", " ") if err != nil { return err } header := &zip.FileHeader{ Name: name, Method: zip.Deflate, } w, err := zw.CreateHeader(header) if err != nil { return err } _, err = w.Write(data) return err }