db

package
v0.0.0-...-8d3d8c4 Latest Latest
Warning

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

Go to latest
Published: Jul 1, 2026 License: Apache-2.0 Imports: 26 Imported by: 0

Documentation

Overview

Package db manages the database connection, migrations, and encryption for the Arkeep server. It supports SQLite (via modernc pure-Go driver, no CGO required) and PostgreSQL. Migrations are embedded in the binary and applied automatically on startup via golang-migrate.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func InitEncryption

func InitEncryption(key []byte) error

InitEncryption sets the AES-256 key used to encrypt and decrypt sensitive fields at rest. key must be exactly 32 bytes (AES-256).

Call this once during application startup, before calling db.New:

if err := db.InitEncryption([]byte(os.Getenv("ARKEEP_SECRET_KEY"))); err != nil {
    log.Fatal(err)
}

func New

func New(cfg Config) (*gorm.DB, error)

New opens a database connection, applies pending migrations, and returns the ready-to-use *gorm.DB instance.

func Ping

func Ping(ctx context.Context, database *gorm.DB) error

Ping verifies that the database connection is still alive.

Types

type Agent

type Agent struct {
	SoftDelete
	Name       string `gorm:"not null"`
	Hostname   string `gorm:"not null"`
	IPAddress  string `gorm:"not null;default:''"`
	OS         string `gorm:"not null;default:''"`
	Arch       string `gorm:"not null;default:''"`
	Version    string `gorm:"not null;default:''"`
	Status     string `gorm:"not null;default:'offline'"` // "online", "offline", "error"
	LastSeenAt *time.Time
	Labels     string `gorm:"type:text;default:'{}'"` // JSON key-value pairs for filtering
	// DockerAvailable is true when the agent can reach the Docker daemon on its host.
	// Advertised by the agent in the Register RPC via AgentCapabilities.docker.
	// Used by the GUI to show or hide the Docker volume source option in the policy form.
	DockerAvailable bool `gorm:"not null;default:false"`
}

Agent represents a registered backup agent running on a remote machine. Agents connect to the server via a persistent gRPC stream (pull pattern) and do not expose any ports. The RegistrationToken is used only during the initial handshake and is cleared after successful registration.

type AuditLog

type AuditLog struct {
	ID           uuid.UUID `gorm:"type:text;primaryKey"`
	CreatedAt    time.Time `gorm:"not null"`
	UserID       uuid.UUID `gorm:"type:text;not null;index"`
	UserEmail    string    `gorm:"not null"`
	Action       string    `gorm:"not null;index"` // e.g. "policy.update", "snapshot.restore"
	ResourceType string    `gorm:"not null;default:''"`
	ResourceID   string    `gorm:"type:text;not null;default:''"`
	Details      string    `gorm:"type:text;not null;default:'{}'"` // JSON
	IPAddress    string    `gorm:"not null;default:''"`
}

AuditLog records every significant mutation performed via the API: who did it, what was changed, when, and from which IP address. The table is append-only — records are never updated or deleted. user_email is stored denormalized so the log remains readable even if the user account is later deleted.

func (*AuditLog) BeforeCreate

func (a *AuditLog) BeforeCreate(tx *gorm.DB) error

BeforeCreate generates a UUID v7 for new audit records (no UpdatedAt — append-only).

func (AuditLog) TableName

func (AuditLog) TableName() string

TableName maps to the migration-created table name.

type Base

type Base struct {
	ID        uuid.UUID `gorm:"type:text;primaryKey"`
	CreatedAt time.Time `gorm:"not null"`
	UpdatedAt time.Time `gorm:"not null"`
}

base contains the common fields shared by all models. ID uses UUID v7 (time-ordered) for efficient B-tree indexing and natural chronological ordering without a separate created_at sort. CreatedAt and UpdatedAt are managed automatically by GORM.

func (*Base) BeforeCreate

func (b *Base) BeforeCreate(tx *gorm.DB) error

BeforeCreate generates a new UUID v7 if the ID is not already set. This ensures every record has a valid time-ordered ID before insertion.

type Config

type Config struct {
	Driver   string // "sqlite" or "postgres"
	DSN      string
	Logger   *zap.Logger
	LogLevel gormlogger.LogLevel
}

Config holds the configuration required to open a database connection. Driver defaults to "sqlite" if left empty.

type Destination

type Destination struct {
	SoftDelete
	Name        string          `gorm:"not null"`
	Type        string          `gorm:"not null"`               // "local", "s3", "sftp", "rest", "rclone"
	Credentials EncryptedString `gorm:"type:text"`              // JSON, encrypted
	Config      string          `gorm:"type:text;default:'{}'"` // JSON, not sensitive
	Enabled     bool            `gorm:"not null;default:true"`
	// RepoSizeBytes is the real deduplicated on-disk size of this destination's
	// restic repository (from `restic stats --mode raw-data`), refreshed after
	// each backup. Zero until the first backup or import completes.
	RepoSizeBytes     int64      `gorm:"not null;default:0"`
	RepoSizeUpdatedAt *time.Time `gorm:""`
}

Destination represents a backup storage target. Credentials are encrypted at rest via EncryptedString. The Config field holds provider-specific settings serialized as JSON (e.g. bucket name, endpoint, region for S3).

type EncryptedString

type EncryptedString string

EncryptedString is a string type that is transparently encrypted with AES-256-GCM before being written to the database, and decrypted after being read. Use it for any sensitive field (credentials, passwords, tokens).

The value stored in the database is a base64-encoded string in the format:

base64(nonce + ciphertext)

An empty EncryptedString is stored as an empty string without encryption.

func (*EncryptedString) Scan

func (e *EncryptedString) Scan(value interface{}) error

Scan implements sql.Scanner. Called by GORM after reading from the database. Decodes the base64 string and decrypts it with AES-256-GCM.

func (EncryptedString) Value

func (e EncryptedString) Value() (driver.Value, error)

Value implements driver.Valuer. Called by GORM before writing to the database. Encrypts the string value with AES-256-GCM and encodes it as base64.

type Job

type Job struct {
	Base
	PolicyID  uuid.UUID `gorm:"type:text;not null;index"`
	AgentID   uuid.UUID `gorm:"type:text;not null;index"`
	Type      string    `gorm:"not null;default:'backup'"`  // "backup", "restore"
	Status    string    `gorm:"not null;default:'pending'"` // "pending", "running", "succeeded", "failed", "cancelled"
	StartedAt *time.Time
	EndedAt   *time.Time
	Error     string `gorm:"type:text;default:''"` // populated on failure

	// Populated manually by GetByIDWithDetails — not managed by GORM.
	Destinations []JobDestination `gorm:"-"`
	Logs         []JobLog         `gorm:"-"`
}

Job represents a single backup execution triggered by the scheduler or manually. Status transitions: pending -> running -> succeeded | failed.

Destinations and Logs are populated by GetByIDWithDetails via manual queries. The gorm:"-" tag prevents GORM from attempting foreign key resolution on these fields, which would fail with uuid.UUID primary keys.

type JobDestination

type JobDestination struct {
	Base
	JobID         uuid.UUID `gorm:"type:text;not null;index"`
	DestinationID uuid.UUID `gorm:"type:text;not null;index"`
	Status        string    `gorm:"not null;default:'pending'"` // mirrors Job.Status
	SnapshotID    string    `gorm:"default:''"`                 // opaque ID returned by the backup engine
	SizeBytes     int64     `gorm:"default:0"`
	StartedAt     *time.Time
	EndedAt       *time.Time
	Error         string `gorm:"type:text;default:''"`
}

JobDestination tracks the result of a backup job for each individual destination. A job can partially succeed if some destinations fail.

type JobLog

type JobLog struct {
	Base
	JobID     uuid.UUID `gorm:"type:text;not null;index"`
	Level     string    `gorm:"not null"` // "info", "warn", "error"
	Message   string    `gorm:"type:text;not null"`
	Timestamp time.Time `gorm:"not null;index"`
}

JobLog stores structured log lines emitted during a job execution. Logs are flushed to the database in batches during execution so that the GUI can show partial logs even for in-progress jobs.

type Notification

type Notification struct {
	Base
	UserID  uuid.UUID `gorm:"type:text;not null;index"`
	Type    string    `gorm:"not null"` // "job_success", "job_failure", "agent_offline", etc.
	Title   string    `gorm:"not null"`
	Body    string    `gorm:"type:text;not null"`
	ReadAt  *time.Time
	Payload string `gorm:"type:text;default:'{}'"` // JSON, extra context for the frontend
}

Notification stores in-app notifications delivered to users via WebSocket. Read notifications are kept for 30 days and then purged by a background job.

type NotificationDelivery

type NotificationDelivery struct {
	Base
	NotificationID uuid.UUID  `gorm:"type:text;not null;index"`
	Type           string     `gorm:"not null"`                   // "email" | "webhook"
	Status         string     `gorm:"not null;default:'pending'"` // "pending" | "sent" | "exhausted"
	Attempts       int        `gorm:"not null;default:0"`
	LastError      string     `gorm:"type:text;not null;default:''"`
	NextRetryAt    *time.Time // nil = ready to process immediately
}

NotificationDelivery tracks the delivery state of a single notification over a single external channel (email or webhook). Each Notification can have at most one NotificationDelivery row per channel type.

Status transitions:

pending → sent      (delivery succeeded)
pending → pending   (retry scheduled after backoff)
pending → exhausted (max 3 retries exceeded, no further attempts)

Rows are automatically removed when the parent Notification is deleted (ON DELETE CASCADE on the foreign key).

func (NotificationDelivery) TableName

func (NotificationDelivery) TableName() string

TableName maps to the migration-created table name.

type OIDCProvider

type OIDCProvider struct {
	Base
	Name         string          `gorm:"not null"`
	Issuer       string          `gorm:"not null"`
	ClientID     string          `gorm:"not null"`
	ClientSecret EncryptedString `gorm:"type:text;not null"`
	Scopes       string          `gorm:"not null;default:'openid email profile'"` // space-separated
	Enabled      bool            `gorm:"not null;default:false"`
}

OIDCProvider stores the configuration for an external OIDC identity provider. ClientSecret is encrypted at rest. Multiple providers are supported. The callback URL is computed server-side as {base_url}/api/v1/auth/oidc/callback and is not stored in the database.

func (OIDCProvider) TableName

func (OIDCProvider) TableName() string

TableName overrides GORM's default naming convention, which would produce "o_id_c_providers" by splitting each uppercase letter as a word boundary.

type PasswordResetToken

type PasswordResetToken struct {
	Base
	UserID    uuid.UUID  `gorm:"type:text;not null;index"`
	TokenHash string     `gorm:"not null;index"` // SHA-256 hex of the raw token
	ExpiresAt time.Time  `gorm:"not null"`
	UsedAt    *time.Time // nil = not yet used
}

PasswordResetToken stores a hashed, single-use token for the self-service password reset flow. Only local accounts can reset their password — OIDC users are managed by their identity provider. The raw token is never stored: only its SHA-256 hash. Tokens expire after a short window and are consumed (UsedAt set) on the first successful reset.

type Policy

type Policy struct {
	SoftDelete
	Name             string          `gorm:"not null"`
	AgentID          uuid.UUID       `gorm:"type:text;not null;index"`
	Schedule         string          `gorm:"not null"` // cron expression
	Enabled          bool            `gorm:"not null;default:true"`
	Sources          string          `gorm:"type:text;not null"` // JSON array of source paths
	RetentionLast    int             `gorm:"not null;default:0"`
	RetentionHourly  int             `gorm:"not null;default:0"`
	RetentionDaily   int             `gorm:"not null;default:0"`
	RetentionWeekly  int             `gorm:"not null;default:0"`
	RetentionMonthly int             `gorm:"not null;default:0"`
	RetentionYearly  int             `gorm:"not null;default:0"`
	RepoPassword     EncryptedString `gorm:"type:text;not null"`     // Restic repository password
	HookPreBackup    string          `gorm:"type:text;default:''"`   // shell command, optional
	HookPostBackup   string          `gorm:"type:text;default:''"`   // shell command, optional
	ExcludePatterns  string          `gorm:"type:text;default:'[]'"` // JSON array of --exclude patterns
	LastRunAt        *time.Time
	NextRunAt        *time.Time

	// Destinations is populated by GetByIDWithDestinations via a manual query.
	// The gorm:"-" tag prevents GORM from attempting foreign key resolution
	// on this field, which would fail with uuid.UUID primary keys.
	Destinations []PolicyDestination `gorm:"-"`
}

Policy defines what to back up, when, and how. It is associated with one agent and one or more destinations via PolicyDestination. The schedule uses standard cron expression syntax (e.g. "0 2 * * *" for 2 AM daily).

Association fields are intentionally absent from this struct. GORM cannot resolve foreign keys when the primary key is uuid.UUID (a custom type). Related records are loaded via explicit queries in the repository layer (see repository/policy.go: GetByIDWithDestinations).

type PolicyDestination

type PolicyDestination struct {
	Base
	PolicyID      uuid.UUID `gorm:"type:text;not null;index"`
	DestinationID uuid.UUID `gorm:"type:text;not null;index"`
	Priority      int       `gorm:"not null;default:0"`
}

PolicyDestination is the join table between Policy and Destination. Priority determines the order in which destinations are tried (lower = first). This enables 3-2-1 backup rules with multiple destinations per policy.

type RefreshToken

type RefreshToken struct {
	Base
	UserID    uuid.UUID `gorm:"type:text;not null;index"`
	TokenHash string    `gorm:"not null;uniqueIndex"` // SHA-256 hex of the raw token
	ExpiresAt time.Time `gorm:"not null;index"`
	RevokedAt *time.Time
	UserAgent string
	IPAddress string
}

RefreshToken stores a hashed refresh token associated with a user session. The raw token is never stored — only its SHA-256 hash. Tokens are rotated on every use and expire after 7 days.

type Setting

type Setting struct {
	Key       string          `gorm:"primaryKey"`
	Value     EncryptedString `gorm:"type:text;not null"`
	UpdatedAt time.Time       `gorm:"not null;autoUpdateTime"`
}

Setting is a generic key-value configuration entry stored in the database. Keys are namespaced by convention (e.g. "smtp.host", "webhook.url"). Sensitive values (e.g. "smtp.password") are encrypted at the application layer via EncryptedString before being persisted.

Setting does not embed base because it uses a string primary key (the key itself) rather than a UUID, and does not need CreatedAt.

type Snapshot

type Snapshot struct {
	Base
	PolicyID      uuid.UUID `gorm:"type:text;not null;index"`
	DestinationID uuid.UUID `gorm:"type:text;not null;index"`
	JobID         uuid.UUID `gorm:"type:text;not null;index"`
	SnapshotID    string    `gorm:"not null;index"` // opaque ID from the backup engine
	// SizeBytes is the real footprint this backup added to the repository
	// (restic data_added_packed), not the logical source size — so it reconciles
	// with the destination's real repo size and never double-counts. Zero when
	// the backup added nothing new (e.g. re-backup of unchanged data).
	SizeBytes  int64     `gorm:"default:0"`
	FileCount  int64     `gorm:"default:0"`
	Tags       string    `gorm:"type:text;default:'[]'"` // JSON array
	Sources    string    `gorm:"type:text;default:'[]'"` // JSON array of paths backed up
	Hostname   string    `gorm:"not null;default:''"`
	IsImported bool      `gorm:"not null;default:false"`
	SnapshotAt time.Time `gorm:"not null;index"`
}

Snapshot represents a point-in-time backup recorded by the backup engine. Snapshots are synced from the engine after each successful job and cached in the database for fast listing and filtering without hitting the engine.

type SoftDelete

type SoftDelete struct {
	Base
	DeletedAt gorm.DeletedAt `gorm:"index"`
}

softDelete extends base with a nullable DeletedAt field for soft deletion. GORM automatically filters out soft-deleted records from all queries unless Unscoped() is used explicitly.

type User

type User struct {
	Base
	Email        string          `gorm:"uniqueIndex;not null"`
	Password     EncryptedString `gorm:"type:text"` // empty for OIDC users
	DisplayName  string          `gorm:"not null"`
	Role         string          `gorm:"not null;default:'user'"`         // "admin" or "user"
	IsActive     bool            `gorm:"not null;default:true"`           // false = account disabled
	OIDCProvider string          `gorm:"column:oidc_provider;default:''"` // provider ID if OIDC user
	OIDCSub      string          `gorm:"column:oidc_sub;default:''"`      // subject claim from OIDC token
	LastLoginAt  *time.Time
}

User represents a local or OIDC-authenticated user. Password is only set for local accounts — OIDC users authenticate via the provider and have an empty Password field.

Jump to

Keyboard shortcuts

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