package provision import ( "context" "database/sql" "encoding/json" "errors" "fmt" "strings" "sub2api-cn-relay-manager/internal/host/sub2api" packdef "sub2api-cn-relay-manager/internal/pack" "sub2api-cn-relay-manager/internal/store/sqlite" ) type PackInstallRequest struct { Pack packdef.LoadedPack } type PackInstallResult struct { Pack sqlite.Pack Providers []sqlite.Provider HostVersion string AlreadyInstalled bool } type PackInstallService struct { store *sqlite.DB host sub2api.HostAdapter } func NewPackInstallService(store *sqlite.DB, host sub2api.HostAdapter) *PackInstallService { return &PackInstallService{store: store, host: host} } func (s *PackInstallService) Install(ctx context.Context, req PackInstallRequest) (PackInstallResult, error) { if s == nil || s.store == nil { return PackInstallResult{}, fmt.Errorf("store is required") } if s.host == nil { return PackInstallResult{}, fmt.Errorf("host adapter is required") } if strings.TrimSpace(req.Pack.Manifest.PackID) == "" { return PackInstallResult{}, fmt.Errorf("pack manifest is required") } hostVersion, err := s.host.GetHostVersion(ctx) if err != nil { return PackInstallResult{}, fmt.Errorf("get host version: %w", err) } if err := packdef.CheckHostCompatibility(req.Pack.Manifest, hostVersion); err != nil { return PackInstallResult{}, err } result := PackInstallResult{HostVersion: hostVersion} if err := s.store.WithTx(ctx, func(queries *sqlite.Queries) error { existing, err := queries.Packs.GetByPackID(ctx, req.Pack.Manifest.PackID) if err == nil { if err := validateExistingPack(existing, req.Pack); err != nil { return err } result.AlreadyInstalled = true } else if !errors.Is(err, sql.ErrNoRows) { return err } packRow, err := buildPackRecord(req.Pack) if err != nil { return err } if _, err := queries.Packs.Upsert(ctx, packRow); err != nil { return err } persistedPack, err := queries.Packs.GetByPackID(ctx, req.Pack.Manifest.PackID) if err != nil { return err } result.Pack = persistedPack providers := make([]sqlite.Provider, 0, len(req.Pack.Providers)) for _, providerManifest := range req.Pack.Providers { providerRow, err := buildProviderRecord(persistedPack.ID, providerManifest) if err != nil { return err } if _, err := queries.Providers.Upsert(ctx, providerRow); err != nil { return err } persistedProvider, err := queries.Providers.GetByPackIDAndProviderID(ctx, persistedPack.ID, providerManifest.ProviderID) if err != nil { return err } providers = append(providers, persistedProvider) } result.Providers = providers return nil }); err != nil { return PackInstallResult{}, err } return result, nil } func validateExistingPack(existing sqlite.Pack, loaded packdef.LoadedPack) error { if strings.TrimSpace(existing.PackID) != strings.TrimSpace(loaded.Manifest.PackID) { return fmt.Errorf("existing pack %q does not match loaded pack %q", existing.PackID, loaded.Manifest.PackID) } return nil } func buildPackRecord(loaded packdef.LoadedPack) (sqlite.Pack, error) { manifestJSON, err := json.Marshal(loaded.Manifest) if err != nil { return sqlite.Pack{}, fmt.Errorf("marshal pack manifest: %w", err) } return sqlite.Pack{ PackID: loaded.Manifest.PackID, Version: loaded.Manifest.Version, Checksum: loaded.Checksum, Vendor: loaded.Manifest.Vendor, TargetHost: loaded.Manifest.TargetHost, MinHostVersion: loaded.Manifest.MinHostVersion, MaxHostVersion: loaded.Manifest.MaxHostVersion, ManifestJSON: string(manifestJSON), }, nil } func buildProviderRecord(packID int64, provider packdef.ProviderManifest) (sqlite.Provider, error) { defaultModelsJSON, err := marshalJSONString(provider.DefaultModels) if err != nil { return sqlite.Provider{}, fmt.Errorf("marshal provider default models: %w", err) } groupTemplateJSON, err := marshalJSONString(provider.GroupTemplate) if err != nil { return sqlite.Provider{}, fmt.Errorf("marshal group template: %w", err) } channelTemplateJSON, err := marshalJSONString(provider.ChannelTemplate) if err != nil { return sqlite.Provider{}, fmt.Errorf("marshal channel template: %w", err) } planTemplateJSON, err := marshalJSONString(provider.PlanTemplate) if err != nil { return sqlite.Provider{}, fmt.Errorf("marshal plan template: %w", err) } importOptionsJSON, err := marshalJSONString(provider.Import) if err != nil { return sqlite.Provider{}, fmt.Errorf("marshal import options: %w", err) } manifestJSON, err := marshalJSONString(provider) if err != nil { return sqlite.Provider{}, fmt.Errorf("marshal provider manifest: %w", err) } return sqlite.Provider{ PackID: packID, ProviderID: provider.ProviderID, DisplayName: provider.DisplayName, BaseURL: provider.BaseURL, Platform: provider.Platform, AccountType: provider.AccountType, DefaultModelsJSON: defaultModelsJSON, SmokeTestModel: provider.SmokeTestModel, GroupTemplateJSON: groupTemplateJSON, ChannelTemplateJSON: channelTemplateJSON, PlanTemplateJSON: planTemplateJSON, ImportOptionsJSON: importOptionsJSON, ManifestJSON: manifestJSON, }, nil } func marshalJSONString(value any) (string, error) { body, err := json.Marshal(value) if err != nil { return "", err } return string(body), nil }