179 lines
7.6 KiB
Go
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()
|
|
}
|