Files
silo-server/internal/api/handlers/catalog_seed.go
T

718 lines
24 KiB
Go

package handlers
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/go-chi/chi/v5"
"github.com/Silo-Server/silo-server/internal/adminjob"
"github.com/Silo-Server/silo-server/internal/catalogseed"
"github.com/Silo-Server/silo-server/internal/notifications"
"github.com/Silo-Server/silo-server/internal/s3client"
)
type CatalogSeedArtifactStore interface {
AdminJobArtifactStore
GetObject(ctx context.Context, bucket, key string) ([]byte, error)
UploadFile(ctx context.Context, bucket, key, path, contentType string) (int64, error)
ListObjectInfos(ctx context.Context, bucket, prefix string) ([]s3client.ObjectInfo, error)
DeleteObject(ctx context.Context, bucket, key string) error
MakeObjectPublic(ctx context.Context, bucket, key string) error
PublicURL(bucket, key string) (string, error)
}
type CatalogSeedHandler struct {
service *catalogseed.Service
jobRepo *adminjob.Repository
store CatalogSeedArtifactStore
localImportDir string
RealtimeHub *notifications.Hub
}
func NewCatalogSeedHandler(service *catalogseed.Service, jobRepo *adminjob.Repository, store CatalogSeedArtifactStore) *CatalogSeedHandler {
return &CatalogSeedHandler{service: service, jobRepo: jobRepo, store: store}
}
type exportCatalogSeedRequest struct {
LibraryIDs []int `json:"library_ids"`
}
type importCatalogSeedResponse struct {
*catalogseed.ImportResult
}
type catalogSeedImportSource struct {
Key string `json:"key"`
SizeBytes int64 `json:"size_bytes"`
LastModified *time.Time `json:"last_modified,omitempty"`
}
type listCatalogSeedImportSourcesResponse struct {
Sources []catalogSeedImportSource `json:"sources"`
}
type catalogSeedErrorResponse struct {
Error string `json:"error"`
Message string `json:"message"`
UnmatchedRoots []string `json:"unmatched_roots,omitempty"`
}
const catalogSeedImportPrefix = "catalog-seeds/"
const remoteCatalogSeedTimeout = 10 * time.Minute
const catalogSeedPublishExpiry = 7 * 24 * time.Hour
func (h *CatalogSeedHandler) HandleExport(w http.ResponseWriter, r *http.Request) {
var req exportCatalogSeedRequest
if r.Body != nil && r.ContentLength != 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) {
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
return
}
}
data, err := h.service.Export(r.Context(), catalogseed.ExportOptions{LibraryIDs: req.LibraryIDs})
if err != nil {
log.Printf("catalog seed export failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to export catalog seed")
return
}
filename := "silo-catalog-seed-" + time.Now().UTC().Format("20060102T150405Z") + ".json.gz"
w.Header().Set("Content-Type", "application/gzip")
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
w.Header().Set("Content-Length", strconv.Itoa(len(data)))
w.WriteHeader(http.StatusOK)
_, _ = w.Write(data)
}
func (h *CatalogSeedHandler) HandleCreateExportJob(w http.ResponseWriter, r *http.Request) {
if h.jobRepo == nil || h.store == nil {
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "Catalog export jobs require the private internal S3 bucket")
return
}
var req exportCatalogSeedRequest
if r.Body != nil && r.ContentLength != 0 {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil && !errors.Is(err, io.EOF) {
writeError(w, http.StatusBadRequest, "bad_request", "Invalid request body")
return
}
}
job, err := h.jobRepo.Create(r.Context(), adminjob.CreateJobInput{
JobType: adminjob.JobTypeCatalogExport,
CreatedByUserID: currentAdminUserID(r),
RequestPayload: catalogseed.ExportOptions{
LibraryIDs: req.LibraryIDs,
},
Message: "Queued catalog export",
})
if err != nil {
var conflict *adminjob.ActiveJobConflictError
switch {
case errors.As(err, &conflict):
var jobsHandler *AdminJobsHandler
if h.jobRepo != nil {
jobsHandler = NewAdminJobsHandler(h.jobRepo, h.store)
}
writeAdminJobConflict(w, "A catalog export is already queued or running", conflict.Job, jobsHandler, r)
default:
log.Printf("catalog seed export job creation failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue catalog export")
}
return
}
jobsHandler := NewAdminJobsHandler(h.jobRepo, h.store)
if h.RealtimeHub != nil {
publishEventJob(r.Context(), h.RealtimeHub.EventsHub(), "job.created", job)
}
writeJSON(w, http.StatusAccepted, adminJobToResponse(r, job, jobsHandler.store))
}
func (h *CatalogSeedHandler) HandlePublishExportJob(w http.ResponseWriter, r *http.Request) {
if h.jobRepo == nil || h.store == nil {
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "Catalog export publishing requires the private internal S3 bucket")
return
}
id := chi.URLParam(r, "id")
if id == "" {
writeError(w, http.StatusBadRequest, "bad_request", "Job ID is required")
return
}
job, err := h.jobRepo.GetByID(r.Context(), id)
if err != nil {
if errors.Is(err, adminjob.ErrJobNotFound) {
writeError(w, http.StatusNotFound, "not_found", "Job not found")
return
}
log.Printf("catalog export publish load failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load catalog export job")
return
}
if job.JobType != adminjob.JobTypeCatalogExport {
writeError(w, http.StatusBadRequest, "bad_request", "Only catalog export jobs can be published")
return
}
if job.Status != adminjob.StatusCompleted {
writeError(w, http.StatusBadRequest, "bad_request", "Only completed catalog export jobs can be published")
return
}
if job.ArtifactBucket == "" || job.ArtifactKey == "" {
writeError(w, http.StatusBadRequest, "bad_request", "Catalog export job does not have an artifact to publish")
return
}
if job.PublicURL != "" {
writeJSON(w, http.StatusOK, adminJobToResponse(r, job, h.store))
return
}
publicURL, err := h.store.PresignGetURL(r.Context(), job.ArtifactBucket, job.ArtifactKey, catalogSeedPublishExpiry)
if err != nil {
log.Printf("catalog export publish failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", fmt.Sprintf("Failed to create catalog export URL: %v", err))
return
}
publishedAt := time.Now().UTC()
if err := h.jobRepo.MarkPublic(r.Context(), job.ID, publicURL, publishedAt); err != nil {
if errors.Is(err, adminjob.ErrJobNotFound) {
writeError(w, http.StatusNotFound, "not_found", "Job not found")
return
}
log.Printf("catalog export mark public failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to persist catalog export public URL")
return
}
job.PublicURL = publicURL
job.PublishedAt = &publishedAt
writeJSON(w, http.StatusOK, adminJobToResponse(r, job, h.store))
}
func (h *CatalogSeedHandler) HandleListImportSources(w http.ResponseWriter, r *http.Request) {
if h.store == nil {
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "Catalog imports from S3 require the private internal S3 bucket")
return
}
objects, err := h.store.ListObjectInfos(r.Context(), h.store.Bucket(), catalogSeedImportPrefix)
if err != nil {
log.Printf("catalog seed import source listing failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list catalog seed import sources")
return
}
sources := make([]catalogSeedImportSource, 0, len(objects))
for _, obj := range objects {
if !strings.HasSuffix(strings.ToLower(obj.Key), ".json.gz") {
continue
}
sources = append(sources, catalogSeedImportSource{
Key: obj.Key,
SizeBytes: obj.SizeBytes,
LastModified: obj.LastModified,
})
}
sort.Slice(sources, func(i, j int) bool {
left := sources[i].LastModified
right := sources[j].LastModified
switch {
case left == nil && right == nil:
return sources[i].Key > sources[j].Key
case left == nil:
return false
case right == nil:
return true
case left.Equal(*right):
return sources[i].Key > sources[j].Key
default:
return left.After(*right)
}
})
writeJSON(w, http.StatusOK, listCatalogSeedImportSourcesResponse{Sources: sources})
}
func (h *CatalogSeedHandler) HandleCreateImportJob(w http.ResponseWriter, r *http.Request) {
if h.jobRepo == nil {
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "Job repository is not configured")
return
}
if err := r.ParseMultipartForm(64 << 20); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", "Invalid multipart form")
return
}
opts, err := parseCatalogImportOptions(r)
if err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
localPath := r.FormValue("local_path")
if localPath != "" {
cleaned := filepath.Clean(localPath)
abs, absErr := filepath.Abs(cleaned)
if absErr != nil || !strings.HasSuffix(strings.ToLower(abs), ".json.gz") {
writeError(w, http.StatusBadRequest, "bad_request", "Local path must point to a .json.gz file")
return
}
info, statErr := os.Stat(abs)
if statErr != nil || !info.Mode().IsRegular() {
writeError(w, http.StatusBadRequest, "bad_request", "Local file not found or not a regular file")
return
}
job, err := h.jobRepo.Create(r.Context(), adminjob.CreateJobInput{
JobType: adminjob.JobTypeCatalogImport,
CreatedByUserID: currentAdminUserID(r),
RequestPayload: adminjob.CatalogImportRequest{
LocalPath: abs,
SourceLabel: filepath.Base(abs),
Options: opts,
},
Message: "Queued catalog import from local file",
})
if err != nil {
var conflict *adminjob.ActiveJobConflictError
if errors.As(err, &conflict) {
jobsHandler := NewAdminJobsHandler(h.jobRepo, h.store)
writeAdminJobConflict(w, "A catalog import is already queued or running", conflict.Job, jobsHandler, r)
} else {
log.Printf("catalog import job creation failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue catalog import")
}
return
}
jobsHandler := NewAdminJobsHandler(h.jobRepo, h.store)
if h.RealtimeHub != nil {
publishEventJob(r.Context(), h.RealtimeHub.EventsHub(), "job.created", job)
}
writeJSON(w, http.StatusAccepted, adminJobToResponse(r, job, jobsHandler.store))
return
}
remoteURL := r.FormValue("remote_url")
if remoteURL != "" {
if _, err := readImportDataFromRemoteURL(r.Context(), remoteURL); err != nil {
if errors.Is(err, errCatalogSeedImportInvalidRemoteURL) {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
writeError(w, http.StatusBadRequest, "bad_request", "Failed to load catalog seed source")
return
}
job, err := h.jobRepo.Create(r.Context(), adminjob.CreateJobInput{
JobType: adminjob.JobTypeCatalogImport,
CreatedByUserID: currentAdminUserID(r),
RequestPayload: adminjob.CatalogImportRequest{
RemoteURL: remoteURL,
SourceLabel: remoteURL,
Options: opts,
},
Message: "Queued catalog import from remote URL",
})
if err != nil {
var conflict *adminjob.ActiveJobConflictError
if errors.As(err, &conflict) {
jobsHandler := NewAdminJobsHandler(h.jobRepo, h.store)
writeAdminJobConflict(w, "A catalog import is already queued or running", conflict.Job, jobsHandler, r)
} else {
log.Printf("catalog import remote job creation failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue catalog import")
}
return
}
jobsHandler := NewAdminJobsHandler(h.jobRepo, h.store)
if h.RealtimeHub != nil {
publishEventJob(r.Context(), h.RealtimeHub.EventsHub(), "job.created", job)
}
writeJSON(w, http.StatusAccepted, adminJobToResponse(r, job, jobsHandler.store))
return
}
// S3-based sources — require store
if h.store == nil {
writeError(w, http.StatusServiceUnavailable, "service_unavailable", "Catalog import jobs require the private internal S3 bucket")
return
}
sourceBucket, sourceKey, sourceLabel, cleanupSource, err := h.resolveImportJobSource(r)
if err != nil {
switch {
case errors.Is(err, errCatalogSeedImportSourceRequired), errors.Is(err, errCatalogSeedImportSourceConflict):
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
case errors.Is(err, errCatalogSeedImportSourceUnavailable):
writeError(w, http.StatusServiceUnavailable, "service_unavailable", err.Error())
case errors.Is(err, adminjob.ErrJobNotFound), errors.Is(err, s3client.ErrNotFound):
writeError(w, http.StatusBadRequest, "bad_request", "Catalog export artifact not found")
default:
log.Printf("catalog import job source failed: %v", err)
writeError(w, http.StatusBadRequest, "bad_request", "Failed to load catalog seed source")
}
return
}
job, err := h.jobRepo.Create(r.Context(), adminjob.CreateJobInput{
JobType: adminjob.JobTypeCatalogImport,
CreatedByUserID: currentAdminUserID(r),
RequestPayload: adminjob.CatalogImportRequest{
SourceBucket: sourceBucket,
SourceKey: sourceKey,
SourceLabel: sourceLabel,
CleanupSource: cleanupSource,
Options: opts,
},
Message: "Queued catalog import",
})
if err != nil {
if cleanupSource {
_ = h.store.DeleteObject(context.Background(), sourceBucket, sourceKey)
}
var conflict *adminjob.ActiveJobConflictError
switch {
case errors.As(err, &conflict):
jobsHandler := NewAdminJobsHandler(h.jobRepo, h.store)
writeAdminJobConflict(w, "A catalog import is already queued or running", conflict.Job, jobsHandler, r)
default:
log.Printf("catalog import job creation failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue catalog import")
}
return
}
jobsHandler := NewAdminJobsHandler(h.jobRepo, h.store)
if h.RealtimeHub != nil {
publishEventJob(r.Context(), h.RealtimeHub.EventsHub(), "job.created", job)
}
writeJSON(w, http.StatusAccepted, adminJobToResponse(r, job, jobsHandler.store))
}
func (h *CatalogSeedHandler) HandleListLocalImportSources(w http.ResponseWriter, r *http.Request) {
dir := h.localImportDirectory()
entries, err := os.ReadDir(dir)
if err != nil {
if os.IsNotExist(err) {
writeJSON(w, http.StatusOK, listCatalogSeedImportSourcesResponse{Sources: []catalogSeedImportSource{}})
return
}
log.Printf("listing local import sources in %s: %v", dir, err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list local import sources")
return
}
sources := make([]catalogSeedImportSource, 0, len(entries))
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(strings.ToLower(entry.Name()), ".json.gz") {
continue
}
info, err := entry.Info()
if err != nil {
continue
}
modTime := info.ModTime().UTC()
sources = append(sources, catalogSeedImportSource{
Key: filepath.Join(dir, entry.Name()),
SizeBytes: info.Size(),
LastModified: &modTime,
})
}
sort.Slice(sources, func(i, j int) bool {
if sources[i].LastModified == nil || sources[j].LastModified == nil {
return sources[i].Key > sources[j].Key
}
return sources[i].LastModified.After(*sources[j].LastModified)
})
writeJSON(w, http.StatusOK, listCatalogSeedImportSourcesResponse{Sources: sources})
}
func (h *CatalogSeedHandler) HandleImport(w http.ResponseWriter, r *http.Request) {
if err := r.ParseMultipartForm(64 << 20); err != nil {
writeError(w, http.StatusBadRequest, "bad_request", "Invalid multipart form")
return
}
data, err := h.readImportData(r)
if err != nil {
switch {
case errors.Is(err, errCatalogSeedImportSourceRequired), errors.Is(err, errCatalogSeedImportSourceConflict):
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
case errors.Is(err, errCatalogSeedImportInvalidRemoteURL):
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
case errors.Is(err, errCatalogSeedImportInvalidLocalPath):
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
case errors.Is(err, errCatalogSeedImportSourceUnavailable):
writeError(w, http.StatusServiceUnavailable, "service_unavailable", err.Error())
case errors.Is(err, adminjob.ErrJobNotFound), errors.Is(err, s3client.ErrNotFound):
writeError(w, http.StatusBadRequest, "bad_request", "Catalog export artifact not found")
default:
log.Printf("catalog seed import source failed: %v", err)
writeError(w, http.StatusBadRequest, "bad_request", "Failed to load catalog seed source")
}
return
}
opts, err := parseCatalogImportOptions(r)
if err != nil {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
result, err := h.service.Import(r.Context(), data, opts)
if err != nil {
var unmatched *catalogseed.UnmatchedRootsError
switch {
case errors.As(err, &unmatched):
writeCatalogSeedError(w, http.StatusBadRequest, "path_rewrite_required", "Catalog seed import requires additional path rewrites", unmatched.Roots)
case errors.Is(err, catalogseed.ErrInvalidBundle):
writeError(w, http.StatusBadRequest, "bad_request", "Invalid catalog seed bundle")
case errors.Is(err, catalogseed.ErrUnsupportedBundleVersion):
writeError(w, http.StatusBadRequest, "bad_request", "Unsupported catalog seed version")
case errors.Is(err, catalogseed.ErrInvalidConflictMode):
writeError(w, http.StatusBadRequest, "bad_request", "Invalid conflict mode")
default:
log.Printf("catalog seed import failed: %v", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to import catalog seed")
}
return
}
writeJSON(w, http.StatusOK, importCatalogSeedResponse{ImportResult: result})
}
func writeCatalogSeedError(w http.ResponseWriter, status int, code, message string, unmatchedRoots []string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(catalogSeedErrorResponse{
Error: code,
Message: message,
UnmatchedRoots: unmatchedRoots,
})
}
const defaultLocalImportDir = "/catalog-seeds"
var (
errCatalogSeedImportSourceRequired = errors.New("Provide exactly one source: local_path, export_job_id, artifact_key, or remote_url")
errCatalogSeedImportSourceConflict = errors.New("Provide only one catalog seed source")
errCatalogSeedImportSourceUnavailable = errors.New("Catalog imports from S3 require the private internal S3 bucket")
errCatalogSeedImportInvalidLocalPath = errors.New("Local path must point to an existing .json.gz file")
errCatalogSeedImportInvalidRemoteURL = errors.New("Remote URL must point to an http(s) .json.gz file")
)
func (h *CatalogSeedHandler) SetLocalImportDir(dir string) {
h.localImportDir = dir
}
func (h *CatalogSeedHandler) localImportDirectory() string {
if h.localImportDir != "" {
return h.localImportDir
}
return defaultLocalImportDir
}
func readLocalImportFile(localPath string) ([]byte, error) {
cleaned := filepath.Clean(localPath)
abs, err := filepath.Abs(cleaned)
if err != nil {
return nil, errCatalogSeedImportInvalidLocalPath
}
if !strings.HasSuffix(strings.ToLower(abs), ".json.gz") {
return nil, errCatalogSeedImportInvalidLocalPath
}
info, err := os.Stat(abs)
if err != nil || !info.Mode().IsRegular() {
return nil, errCatalogSeedImportInvalidLocalPath
}
data, err := os.ReadFile(abs)
if err != nil {
return nil, fmt.Errorf("reading local import file: %w", err)
}
return data, nil
}
func (h *CatalogSeedHandler) readImportData(r *http.Request) ([]byte, error) {
localPath := r.FormValue("local_path")
jobID := r.FormValue("export_job_id")
artifactKey := r.FormValue("artifact_key")
remoteURL := r.FormValue("remote_url")
hasLocal := localPath != ""
hasJob := jobID != ""
hasArtifact := artifactKey != ""
hasRemoteURL := remoteURL != ""
sourceCount := 0
if hasLocal {
sourceCount++
}
if hasJob {
sourceCount++
}
if hasArtifact {
sourceCount++
}
if hasRemoteURL {
sourceCount++
}
switch {
case sourceCount == 0:
return nil, errCatalogSeedImportSourceRequired
case sourceCount > 1:
return nil, errCatalogSeedImportSourceConflict
case hasLocal:
return readLocalImportFile(localPath)
case hasRemoteURL:
return readImportDataFromRemoteURL(r.Context(), remoteURL)
case hasArtifact:
return h.readImportDataFromArtifactKey(r.Context(), artifactKey)
default:
return h.readImportDataFromExportJob(r.Context(), jobID)
}
}
func (h *CatalogSeedHandler) readImportDataFromExportJob(ctx context.Context, jobID string) ([]byte, error) {
bucket, key, err := h.resolveExportJobArtifactRef(ctx, jobID)
if err != nil {
return nil, err
}
data, err := h.store.GetObject(ctx, bucket, key)
if err != nil {
return nil, err
}
return data, nil
}
func (h *CatalogSeedHandler) readImportDataFromArtifactKey(ctx context.Context, artifactKey string) ([]byte, error) {
if h.store == nil {
return nil, errCatalogSeedImportSourceUnavailable
}
return h.store.GetObject(ctx, h.store.Bucket(), artifactKey)
}
func readImportDataFromRemoteURL(ctx context.Context, remoteURL string) ([]byte, error) {
parsed, err := url.Parse(remoteURL)
if err != nil {
return nil, errCatalogSeedImportInvalidRemoteURL
}
if parsed.Scheme != "http" && parsed.Scheme != "https" {
return nil, errCatalogSeedImportInvalidRemoteURL
}
if !strings.HasSuffix(strings.ToLower(parsed.Path), ".json.gz") {
return nil, errCatalogSeedImportInvalidRemoteURL
}
req, err := http.NewRequestWithContext(ctx, http.MethodGet, remoteURL, nil)
if err != nil {
return nil, fmt.Errorf("building remote catalog seed request: %w", err)
}
client := &http.Client{Timeout: remoteCatalogSeedTimeout}
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("downloading remote catalog seed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("downloading remote catalog seed: unexpected status %d", resp.StatusCode)
}
data, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading remote catalog seed: %w", err)
}
return data, nil
}
func (h *CatalogSeedHandler) resolveImportJobSource(r *http.Request) (bucket string, key string, label string, cleanup bool, err error) {
jobID := r.FormValue("export_job_id")
artifactKey := r.FormValue("artifact_key")
remoteURL := r.FormValue("remote_url")
hasJob := jobID != ""
hasArtifact := artifactKey != ""
hasRemoteURL := remoteURL != ""
sourceCount := 0
if hasJob {
sourceCount++
}
if hasArtifact {
sourceCount++
}
if hasRemoteURL {
sourceCount++
}
switch {
case sourceCount == 0:
return "", "", "", false, errCatalogSeedImportSourceRequired
case sourceCount > 1:
return "", "", "", false, errCatalogSeedImportSourceConflict
case hasRemoteURL:
return "", "", remoteURL, false, errCatalogSeedImportInvalidRemoteURL
case hasArtifact:
if h.store == nil {
return "", "", "", false, errCatalogSeedImportSourceUnavailable
}
return h.store.Bucket(), artifactKey, filepath.Base(artifactKey), false, nil
default:
bucket, key, resolveErr := h.resolveExportJobArtifactRef(r.Context(), jobID)
if resolveErr != nil {
return "", "", "", false, resolveErr
}
return bucket, key, "Export job " + jobID, false, nil
}
}
func (h *CatalogSeedHandler) resolveExportJobArtifactRef(ctx context.Context, jobID string) (string, string, error) {
if h.jobRepo == nil || h.store == nil {
return "", "", errCatalogSeedImportSourceUnavailable
}
job, err := h.jobRepo.GetByID(ctx, jobID)
if err != nil {
return "", "", err
}
if job.JobType != adminjob.JobTypeCatalogExport || job.Status != adminjob.StatusCompleted || job.ArtifactBucket == "" || job.ArtifactKey == "" {
return "", "", fmt.Errorf("catalog export job %s is not ready for import", jobID)
}
return job.ArtifactBucket, job.ArtifactKey, nil
}
func parseCatalogImportOptions(r *http.Request) (catalogseed.ImportOptions, error) {
var rewrites []catalogseed.PathRewrite
if raw := r.FormValue("path_rewrites"); raw != "" {
if err := json.Unmarshal([]byte(raw), &rewrites); err != nil {
return catalogseed.ImportOptions{}, errors.New("Invalid path_rewrites")
}
}
return catalogseed.ImportOptions{
ConflictMode: catalogseed.ConflictMode(r.FormValue("conflict_mode")),
PathRewrites: rewrites,
}, nil
}