Co-authored-by: Jeffrey Smith <jasafpro@gmail.com> Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
128 lines
3.3 KiB
Go
128 lines
3.3 KiB
Go
package handlers
|
|
|
|
// package_export.go — v0.30.0 CS3
|
|
//
|
|
// Exports an installed package as a downloadable .pkg archive for
|
|
// cross-instance sharing. Includes manifest, static assets, and
|
|
// Starlark scripts. Does NOT include extension table data or secrets.
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"git.gobha.me/xcaliber/chat-switchboard/store"
|
|
)
|
|
|
|
// PackageExportHandler handles package export operations.
|
|
type PackageExportHandler struct {
|
|
stores store.Stores
|
|
packagesDir string
|
|
}
|
|
|
|
// NewPackageExportHandler creates a new export handler.
|
|
func NewPackageExportHandler(s store.Stores, packagesDir string) *PackageExportHandler {
|
|
return &PackageExportHandler{stores: s, packagesDir: packagesDir}
|
|
}
|
|
|
|
// ExportPackage downloads an installed package as a .pkg archive.
|
|
// GET /api/v1/admin/packages/:id/export
|
|
func (h *PackageExportHandler) ExportPackage(c *gin.Context) {
|
|
id := c.Param("id")
|
|
pkg, err := h.stores.Packages.Get(c.Request.Context(), id)
|
|
if err != nil || pkg == nil {
|
|
c.JSON(http.StatusNotFound, gin.H{"error": "package not found"})
|
|
return
|
|
}
|
|
|
|
if pkg.Source == "core" {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": "cannot export core packages"})
|
|
return
|
|
}
|
|
|
|
// Build manifest for export
|
|
manifest := make(map[string]any, len(pkg.Manifest))
|
|
for k, v := range pkg.Manifest {
|
|
// Skip internal fields that shouldn't be exported
|
|
if k == "_script" {
|
|
continue
|
|
}
|
|
manifest[k] = v
|
|
}
|
|
|
|
// Include package_settings as default_settings so importing instance
|
|
// gets admin defaults (but not secrets)
|
|
if len(pkg.PackageSettings) > 0 && string(pkg.PackageSettings) != "{}" {
|
|
var ps map[string]any
|
|
if json.Unmarshal(pkg.PackageSettings, &ps) == nil && len(ps) > 0 {
|
|
manifest["default_settings"] = ps
|
|
}
|
|
}
|
|
|
|
// Set response headers
|
|
filename := fmt.Sprintf("%s-%s.pkg", id, pkg.Version)
|
|
c.Header("Content-Type", "application/zip")
|
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
|
|
|
// Create zip writer directly to response
|
|
zw := zip.NewWriter(c.Writer)
|
|
defer zw.Close()
|
|
|
|
// Write manifest.json
|
|
manifestJSON, err := json.MarshalIndent(manifest, "", " ")
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to serialize manifest"})
|
|
return
|
|
}
|
|
mf, err := zw.Create("manifest.json")
|
|
if err != nil {
|
|
return
|
|
}
|
|
mf.Write(manifestJSON)
|
|
|
|
// Include Starlark script as script.star if present
|
|
if script, ok := pkg.Manifest["_starlark_script"].(string); ok && script != "" {
|
|
sf, err := zw.Create("script.star")
|
|
if err == nil {
|
|
sf.Write([]byte(script))
|
|
}
|
|
}
|
|
|
|
// Walk packagesDir/{id}/ for static assets
|
|
if h.packagesDir != "" {
|
|
assetsDir := filepath.Join(h.packagesDir, id)
|
|
if info, err := os.Stat(assetsDir); err == nil && info.IsDir() {
|
|
filepath.Walk(assetsDir, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil || info.IsDir() {
|
|
return nil
|
|
}
|
|
relPath, err := filepath.Rel(assetsDir, path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
// Normalize path separators for zip
|
|
relPath = strings.ReplaceAll(relPath, string(filepath.Separator), "/")
|
|
|
|
f, err := zw.Create(relPath)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
src, err := os.Open(path)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
defer src.Close()
|
|
io.Copy(f, src)
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
}
|