Files
DoGaMa-serv/internal/persistence/sqlite/backup.go
T

179 lines
7.6 KiB
Go

package sqlite
import (
"context"
"database/sql"
"encoding/json"
"errors"
"fmt"
"time"
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/backup"
)
func (r *Repository) BeginBackup(ctx context.Context, value backup.Backup, operationID, actorID string) error {
var operation, actor any
if operationID != "" {
operation = operationID
}
if actorID != "" {
actor = actorID
}
_, err := r.db.ExecContext(ctx, `INSERT INTO backups(id, instance_id, operation_id, origin, status, created_by, created_at)
VALUES (?, ?, ?, ?, 'creating', ?, ?)`, value.ID, value.InstanceID, operation, value.Origin, actor, value.CreatedAt)
if err != nil {
return fmt.Errorf("begin backup metadata: %w", err)
}
return nil
}
func (r *Repository) CompleteBackup(ctx context.Context, id, relativePath string, size int64, checksum string, manifest backup.Manifest) error {
encoded, err := json.Marshal(manifest)
if err != nil {
return fmt.Errorf("encode backup manifest: %w", err)
}
now := r.now().UTC().Format(time.RFC3339Nano)
result, err := r.db.ExecContext(ctx, `UPDATE backups SET status='available', relative_path=?, size_bytes=?, sha256=?, manifest_json=?, completed_at=?, error_code=NULL
WHERE id=? AND status='creating'`, relativePath, size, checksum, string(encoded), now, id)
if err != nil {
return fmt.Errorf("complete backup metadata: %w", err)
}
changed, _ := result.RowsAffected()
if changed != 1 {
return backup.ErrInvalidState
}
return nil
}
func (r *Repository) FailBackup(ctx context.Context, id, code string) error {
result, err := r.db.ExecContext(ctx, `UPDATE backups SET status='failed', error_code=?, completed_at=? WHERE id=? AND status='creating'`, code, r.now().UTC().Format(time.RFC3339Nano), id)
if err != nil {
return fmt.Errorf("fail backup metadata: %w", err)
}
changed, _ := result.RowsAffected()
if changed != 1 {
return backup.ErrInvalidState
}
return nil
}
func (r *Repository) ListBackups(ctx context.Context, instanceID string) ([]backup.Backup, error) {
rows, err := r.db.QueryContext(ctx, `SELECT id, instance_id, origin, status, COALESCE(relative_path, ''), COALESCE(size_bytes, 0), COALESCE(sha256, ''), created_at, COALESCE(completed_at, ''), COALESCE(error_code, '')
FROM backups WHERE instance_id=? AND status!='deleted' ORDER BY created_at DESC, id`, instanceID)
if err != nil {
return nil, fmt.Errorf("list backups: %w", err)
}
defer rows.Close()
var result []backup.Backup
for rows.Next() {
var value backup.Backup
if err := rows.Scan(&value.ID, &value.InstanceID, &value.Origin, &value.Status, &value.RelativePath, &value.SizeBytes, &value.SHA256, &value.CreatedAt, &value.CompletedAt, &value.ErrorCode); err != nil {
return nil, fmt.Errorf("scan backup: %w", err)
}
result = append(result, value)
}
return result, rows.Err()
}
func (r *Repository) GetBackup(ctx context.Context, instanceID, id string) (backup.Backup, backup.Manifest, error) {
var value backup.Backup
var manifestJSON string
err := r.db.QueryRowContext(ctx, `SELECT id, instance_id, origin, status, COALESCE(relative_path, ''), COALESCE(size_bytes, 0), COALESCE(sha256, ''), created_at, COALESCE(completed_at, ''), COALESCE(error_code, ''), COALESCE(manifest_json, '')
FROM backups WHERE id=? AND instance_id=? AND status!='deleted'`, id, instanceID).Scan(&value.ID, &value.InstanceID, &value.Origin, &value.Status, &value.RelativePath, &value.SizeBytes, &value.SHA256, &value.CreatedAt, &value.CompletedAt, &value.ErrorCode, &manifestJSON)
if errors.Is(err, sql.ErrNoRows) {
return backup.Backup{}, backup.Manifest{}, backup.ErrNotFound
}
if err != nil {
return backup.Backup{}, backup.Manifest{}, fmt.Errorf("get backup: %w", err)
}
var manifest backup.Manifest
if manifestJSON != "" {
if err := json.Unmarshal([]byte(manifestJSON), &manifest); err != nil {
return backup.Backup{}, backup.Manifest{}, backup.ErrIntegrity
}
}
return value, manifest, nil
}
func (r *Repository) RetentionCandidates(ctx context.Context, instanceID string, keep int) ([]backup.Backup, error) {
rows, err := r.db.QueryContext(ctx, `SELECT id, instance_id, origin, status, relative_path, size_bytes, sha256, created_at, completed_at, ''
FROM backups WHERE instance_id=? AND origin='scheduled' AND status='available'
ORDER BY created_at DESC, id DESC LIMIT -1 OFFSET ?`, instanceID, keep)
if err != nil {
return nil, fmt.Errorf("list retention candidates: %w", err)
}
defer rows.Close()
var result []backup.Backup
for rows.Next() {
var value backup.Backup
if err := rows.Scan(&value.ID, &value.InstanceID, &value.Origin, &value.Status, &value.RelativePath, &value.SizeBytes, &value.SHA256, &value.CreatedAt, &value.CompletedAt, &value.ErrorCode); err != nil {
return nil, err
}
result = append(result, value)
}
return result, rows.Err()
}
func (r *Repository) MarkBackupDeleted(ctx context.Context, id string) error {
result, err := r.db.ExecContext(ctx, `UPDATE backups SET status='deleted', relative_path=NULL, deleted_at=? WHERE id=? AND status='available'`, r.now().UTC().Format(time.RFC3339Nano), id)
if err != nil {
return fmt.Errorf("mark backup deleted: %w", err)
}
changed, _ := result.RowsAffected()
if changed != 1 {
return backup.ErrInvalidState
}
return nil
}
func (r *Repository) GetBackupPolicy(ctx context.Context, instanceID string) (backup.Policy, error) {
var value backup.Policy
var enabled int
err := r.db.QueryRowContext(ctx, `SELECT instance_id, enabled, COALESCE(cron_expression, ''), timezone, retention_count, COALESCE(next_run_at, '') FROM backup_policies WHERE instance_id=?`, instanceID).Scan(&value.InstanceID, &enabled, &value.CronExpression, &value.Timezone, &value.RetentionCount, &value.NextRunAt)
if errors.Is(err, sql.ErrNoRows) {
instanceValue, instanceErr := r.GetInstance(ctx, instanceID)
if instanceErr != nil {
return backup.Policy{}, instanceErr
}
return backup.Policy{InstanceID: instanceID, Timezone: "UTC", RetentionCount: instanceValue.Preview.Backup.RetentionCount}, nil
}
if err != nil {
return backup.Policy{}, fmt.Errorf("get backup policy: %w", err)
}
value.Enabled = enabled != 0
return value, nil
}
func (r *Repository) SetBackupPolicy(ctx context.Context, value backup.Policy) error {
enabled := 0
if value.Enabled {
enabled = 1
}
now := r.now().UTC().Format(time.RFC3339Nano)
_, err := r.db.ExecContext(ctx, `INSERT INTO backup_policies(instance_id, enabled, cron_expression, timezone, retention_count, next_run_at, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(instance_id) DO UPDATE SET enabled=excluded.enabled, cron_expression=excluded.cron_expression, timezone=excluded.timezone, retention_count=excluded.retention_count, next_run_at=excluded.next_run_at, updated_at=excluded.updated_at`, value.InstanceID, enabled, nullable(value.CronExpression), value.Timezone, value.RetentionCount, nullable(value.NextRunAt), now, now)
if err != nil {
return fmt.Errorf("set backup policy: %w", err)
}
return nil
}
func (r *Repository) ListDueBackupPolicies(ctx context.Context, now string) ([]backup.Policy, error) {
rows, err := r.db.QueryContext(ctx, `SELECT instance_id, enabled, cron_expression, timezone, retention_count, next_run_at FROM backup_policies WHERE enabled=1 AND next_run_at<=? ORDER BY next_run_at`, now)
if err != nil {
return nil, fmt.Errorf("list due backup policies: %w", err)
}
defer rows.Close()
var result []backup.Policy
for rows.Next() {
var value backup.Policy
var enabled int
if err := rows.Scan(&value.InstanceID, &enabled, &value.CronExpression, &value.Timezone, &value.RetentionCount, &value.NextRunAt); err != nil {
return nil, err
}
value.Enabled = enabled != 0
result = append(result, value)
}
return result, rows.Err()
}