settingsio

package
v1.0.6 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 7, 2026 License: GPL-3.0 Imports: 23 Imported by: 0

Documentation

Overview

Package settingsio: cross-instance user export/import.

Users are exported by username (the only key that is portable across instances; ids are uuid-per-instance). On import, users that already exist on the target are left untouched -- the operator's existing setup wins over the envelope. Users absent on the target are recreated so that downstream rows (api tokens, user preferences) can attribute back to them without a fallback.

This file lives alongside tokens.go and libraries.go to keep the per-domain export/import helpers physically together; the dispatch from Service.Export / Service.Import is in export.go.

Index

Constants

View Source
const CurrentEnvelopeVersion = "1.3"

CurrentEnvelopeVersion is the version emitted by Export. Bump whenever the Payload schema changes in a way that older binaries cannot safely round-trip.

  • "1.0": original format (settings, connections, platform profiles, webhooks, provider keys, priorities)
  • "1.1": adds rules, scraper_configs, user_preferences, plaintext summary
  • "1.2": adds libraries (connection refs remapped by type+url) and api_tokens (token_hash + metadata only; never plaintext)
  • "1.3": adds users block so cross-instance restore can recreate absent owners before remapping api_tokens / user_preferences (#1283). The password_hash inside Users is a bcrypt digest -- never plaintext -- and only crosses the wire inside the passphrase-encrypted envelope.

Variables

View Source
var ErrUnsupportedVersion = errors.New("unsupported export format version")

ErrUnsupportedVersion is returned by Import when the envelope's Version field is not one of the formats this binary knows how to read.

View Source
var ErrWrongPassphrase = errors.New("incorrect passphrase or corrupted backup file")

ErrWrongPassphrase is returned by Import when the AES-GCM tag verification fails, meaning the supplied passphrase does not match the one used during export. Callers may use errors.Is to distinguish this case from other import errors and surface a human-friendly hint.

Functions

This section is empty.

Types

type APITokenExport added in v1.0.5

type APITokenExport struct {
	Name       string `json:"name"`
	TokenHash  string `json:"token_hash"`
	Scopes     string `json:"scopes"`
	Username   string `json:"username"`
	CreatedAt  string `json:"created_at"`
	LastUsedAt string `json:"last_used_at,omitempty"`
	RevokedAt  string `json:"revoked_at,omitempty"`
	Status     string `json:"status"`
}

APITokenExport carries the persisted API-token row in a form portable across instances. The plaintext token is never persisted in the DB, so this struct cannot expose it; only the stored hash crosses the wire, and only inside the passphrase-encrypted envelope. Username replaces user_id because user IDs are generated per-instance (same remap pattern as user_preferences).

type ConnectionExport

type ConnectionExport struct {
	Name                 string `json:"name"`
	Type                 string `json:"type"`
	URL                  string `json:"url"`
	APIKey               string `json:"api_key"`
	Enabled              bool   `json:"enabled"`
	FeatureLibraryImport bool   `json:"feature_library_import"`
	FeatureNFOWrite      bool   `json:"feature_nfo_write"`
	FeatureImageWrite    bool   `json:"feature_image_write"`
}

ConnectionExport is a connection with its API key decrypted for export.

type Envelope

type Envelope struct {
	Version    string        `json:"version"`
	AppVersion string        `json:"app_version"`
	CreatedAt  string        `json:"created_at"`
	Salt       string        `json:"salt"` // base64-encoded PBKDF2 salt
	Data       string        `json:"data"` // base64-encoded nonce+ciphertext
	Summary    *ImportResult `json:"summary,omitempty"`
}

Envelope is the outer JSON wrapper for an exported settings file. Summary is plaintext metadata -- section counts do not reveal secrets (which remain inside the encrypted Data blob) but give the user a sanity-check that the export matches their expectations.

type ImportOptions added in v1.0.5

type ImportOptions struct {
	// AdminFallbackTokens, when true, attributes api_tokens whose original
	// username remains absent on the target (after the envelope's Users
	// block has been applied) to ImportingAdminUserID. Each reassignment
	// increments ImportResult.OwnershipReassigned so the audit is visible
	// to the operator.
	//
	// This is opt-in because silent ownership reassignment surprises
	// operators who rely on the historical "skip unknown owner" semantics
	// for cross-environment exports (e.g. prod -> staging clones).
	AdminFallbackTokens bool

	// ImportingAdminUserID is the user_id to attribute reassigned tokens to
	// when AdminFallbackTokens is true. The HTTP handler resolves it from
	// the authenticated session before calling ImportWithOptions.
	ImportingAdminUserID string
}

ImportOptions controls optional behaviors at import time. The zero value reproduces the historical behavior: tokens whose owning username is absent on the target are skipped (their count surfaces via APITokensSkipped) and no automatic ownership reassignment occurs.

type ImportResult

type ImportResult struct {
	Settings         int `json:"settings"`
	Connections      int `json:"connections"`
	Profiles         int `json:"platform_profiles"`
	Webhooks         int `json:"webhooks"`
	ProviderKeys     int `json:"provider_keys"`
	Priorities       int `json:"priorities"`
	Rules            int `json:"rules"`
	ScraperConfigs   int `json:"scraper_configs"`
	UserPreferences  int `json:"user_preferences"`
	Libraries        int `json:"libraries"`
	LibrariesSkipped int `json:"libraries_skipped,omitempty"`
	APITokens        int `json:"api_tokens"`
	APITokensSkipped int `json:"api_tokens_skipped,omitempty"`
	// UsersImported counts user rows recreated from the envelope on import
	// because they were absent on the target instance (#1283). Users that
	// already existed on the target are NOT counted -- their rows are left
	// untouched so the operator's local setup wins over the envelope.
	UsersImported int `json:"users_imported,omitempty"`
	// OwnershipReassigned counts api_tokens whose original owner is absent
	// on the target AND who were attributed to the importing admin via the
	// admin-fallback opt-in. This is a deliberate ownership change and is
	// surfaced in the result so it cannot be silent (#1283).
	OwnershipReassigned int `json:"ownership_reassigned,omitempty"`
}

ImportResult summarizes what was imported. Skip counters track rows that were intentionally omitted (e.g. a library whose connection is missing on the target, or a token whose owning user is absent); they let callers surface a per-domain "imported / skipped" breakdown without reparsing logs.

type LibraryExport added in v1.0.5

type LibraryExport struct {
	Name           string `json:"name"`
	Path           string `json:"path"`
	Type           string `json:"type"`                      // "regular" | "classical"
	Source         string `json:"source"`                    // "manual" | "emby" | "jellyfin" | "lidarr"
	ConnectionType string `json:"connection_type,omitempty"` // for remap on import
	ConnectionURL  string `json:"connection_url,omitempty"`  // for remap on import
	ExternalID     string `json:"external_id,omitempty"`
	FSWatch        int    `json:"fs_watch"`
	FSPollInterval int    `json:"fs_poll_interval"`
	NFOLockData    bool   `json:"nfo_lock_data,omitempty"`
}

LibraryExport carries the persistent configuration of a single library row. Connection IDs are not exported because they are generated per-instance; instead the owning connection's (type, url) is carried alongside so the import step can remap to the local connection_id. Runtime-detected shared-FS state (status / evidence / peer ids) is intentionally omitted -- the target instance must re-detect on its own scan to avoid carrying stale cluster geometry across the boundary.

type Payload

type Payload struct {
	Settings           map[string]string     `json:"settings"`
	Connections        []ConnectionExport    `json:"connections"`
	PlatformProfiles   []platform.Profile    `json:"platform_profiles"`
	Webhooks           []webhook.Webhook     `json:"webhooks"`
	ProviderKeys       map[string]string     `json:"provider_keys"`
	ProviderPriorities []PriorityExport      `json:"provider_priorities"`
	Rules              []RuleExport          `json:"rules,omitempty"`
	ScraperConfigs     []ScraperConfigExport `json:"scraper_configs,omitempty"`
	UserPreferences    []UserPrefsExport     `json:"user_preferences,omitempty"`
	Libraries          []LibraryExport       `json:"libraries,omitempty"`
	APITokens          []APITokenExport      `json:"api_tokens,omitempty"`
	Users              []UserExport          `json:"users,omitempty"`
}

Payload is the decrypted inner content of an export.

type PriorityExport

type PriorityExport struct {
	Field     string                  `json:"field"`
	Providers []provider.ProviderName `json:"providers"`
	Disabled  []provider.ProviderName `json:"disabled,omitempty"`
}

PriorityExport holds a field's provider priority list.

type RuleExport added in v0.9.6

type RuleExport struct {
	ID             string          `json:"id"`
	Enabled        bool            `json:"enabled"`
	AutomationMode string          `json:"automation_mode"`
	Config         rule.RuleConfig `json:"config"`
}

RuleExport holds the mutable configuration of a single rule. Only enabled, automation_mode, and config are exported; immutable fields (name, description, category) are not overwritten on import because they are defined by the application binary, not by the user.

type ScraperConfigExport added in v0.9.6

type ScraperConfigExport struct {
	Scope     string                `json:"scope"`
	Config    scraper.ScraperConfig `json:"config"`
	Overrides *scraper.Overrides    `json:"overrides,omitempty"`
}

ScraperConfigExport holds one scope's scraper configuration and its override set. The scope string identifies global ("global") or a connection-scoped entry.

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service handles settings export and import.

func NewService

func NewService(
	db *sql.DB,
	ps *provider.SettingsService,
	cs *connection.Service,
	ps2 *platform.Service,
	ws *webhook.Service,
) *Service

NewService creates a settings export/import service.

func (*Service) Export

func (s *Service) Export(ctx context.Context, passphrase string) (*Envelope, error)

Export collects all settings data, encrypts it with the given passphrase, and returns an Envelope. The passphrase is used with PBKDF2 to derive an AES-256-GCM key, making exports portable across instances.

func (*Service) Import

func (s *Service) Import(ctx context.Context, env *Envelope, passphrase string) (*ImportResult, error)

Import decrypts and applies settings from an Envelope using the given passphrase. The passphrase must match the one used during export.

This is a thin wrapper around ImportWithOptions that preserves the historical default (no admin-fallback for token ownership). New callers that need to opt into admin-fallback should call ImportWithOptions directly.

func (*Service) ImportWithOptions added in v1.0.5

func (s *Service) ImportWithOptions(ctx context.Context, env *Envelope, passphrase string, opts ImportOptions) (*ImportResult, error)

ImportWithOptions decrypts and applies settings from an Envelope, honoring the supplied ImportOptions. See ImportOptions for the available knobs.

func (*Service) WithRuleService added in v0.9.6

func (s *Service) WithRuleService(rs *rule.Service) *Service

WithRuleService attaches a rule service so rule configuration is included in export/import. If not set, rule config is silently skipped.

func (*Service) WithScraperService added in v0.9.6

func (s *Service) WithScraperService(ss *scraper.Service) *Service

WithScraperService attaches a scraper service so scraper configuration is included in export/import. If not set, scraper config is silently skipped.

type UserExport added in v1.0.5

type UserExport struct {
	Username     string `json:"username"`
	DisplayName  string `json:"display_name,omitempty"`
	PasswordHash string `json:"password_hash,omitempty"`
	Role         string `json:"role"`
	AuthProvider string `json:"auth_provider,omitempty"`
	ProviderID   string `json:"provider_id,omitempty"`
	IsActive     bool   `json:"is_active"`
	IsProtected  bool   `json:"is_protected,omitempty"`
	CreatedAt    string `json:"created_at"`
}

UserExport carries a single user row in a form portable across instances. Internal ids (id, invited_by) are intentionally omitted because they are regenerated per-instance; downstream rows (api_tokens, user_preferences) remap by username instead.

PasswordHash is preserved for local-auth users so credentials survive a restore. The hash is a bcrypt digest, never plaintext, and it only crosses the wire inside the passphrase-encrypted envelope. Federated-only users have an empty hash (their schema row already stores ”).

type UserPrefsExport added in v0.9.6

type UserPrefsExport struct {
	Username    string            `json:"username"`
	Preferences map[string]string `json:"preferences"`
}

UserPrefsExport holds the full preference map for a single user, identified by username rather than internal ID so the export is portable across instances.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL