first commit

This commit is contained in:
Awuqing
2026-03-17 13:29:09 +08:00
commit eadd3f8961
219 changed files with 22394 additions and 0 deletions

View File

@@ -0,0 +1,183 @@
package repository
import (
"context"
"errors"
"time"
"backupx/server/internal/model"
"gorm.io/gorm"
)
type BackupRecordListOptions struct {
TaskID *uint
Status string
DateFrom *time.Time
DateTo *time.Time
Limit int
Offset int
}
type BackupTimelinePoint struct {
Date string `json:"date"`
Total int64 `json:"total"`
Success int64 `json:"success"`
Failed int64 `json:"failed"`
}
type BackupStorageUsageItem struct {
StorageTargetID uint `json:"storageTargetId"`
TotalSize int64 `json:"totalSize"`
}
type BackupRecordRepository interface {
List(context.Context, BackupRecordListOptions) ([]model.BackupRecord, error)
FindByID(context.Context, uint) (*model.BackupRecord, error)
Create(context.Context, *model.BackupRecord) error
Update(context.Context, *model.BackupRecord) error
Delete(context.Context, uint) error
ListRecent(context.Context, int) ([]model.BackupRecord, error)
ListSuccessfulByTask(context.Context, uint) ([]model.BackupRecord, error)
Count(context.Context) (int64, error)
CountSince(context.Context, time.Time) (int64, error)
CountSuccessSince(context.Context, time.Time) (int64, error)
SumFileSize(context.Context) (int64, error)
TimelineSince(context.Context, time.Time) ([]BackupTimelinePoint, error)
StorageUsage(context.Context) ([]BackupStorageUsageItem, error)
}
type GormBackupRecordRepository struct {
db *gorm.DB
}
func NewBackupRecordRepository(db *gorm.DB) *GormBackupRecordRepository {
return &GormBackupRecordRepository{db: db}
}
func (r *GormBackupRecordRepository) List(ctx context.Context, options BackupRecordListOptions) ([]model.BackupRecord, error) {
query := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Preload("Task").Preload("Task.StorageTarget").Order("started_at desc")
if options.TaskID != nil {
query = query.Where("task_id = ?", *options.TaskID)
}
if options.Status != "" {
query = query.Where("status = ?", options.Status)
}
if options.DateFrom != nil {
query = query.Where("started_at >= ?", options.DateFrom.UTC())
}
if options.DateTo != nil {
query = query.Where("started_at <= ?", options.DateTo.UTC())
}
if options.Limit > 0 {
query = query.Limit(options.Limit)
}
if options.Offset > 0 {
query = query.Offset(options.Offset)
}
var items []model.BackupRecord
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormBackupRecordRepository) FindByID(ctx context.Context, id uint) (*model.BackupRecord, error) {
var item model.BackupRecord
if err := r.db.WithContext(ctx).Preload("Task").Preload("Task.StorageTarget").First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormBackupRecordRepository) Create(ctx context.Context, item *model.BackupRecord) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *GormBackupRecordRepository) Update(ctx context.Context, item *model.BackupRecord) error {
return r.db.WithContext(ctx).Save(item).Error
}
func (r *GormBackupRecordRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.BackupRecord{}, id).Error
}
func (r *GormBackupRecordRepository) ListRecent(ctx context.Context, limit int) ([]model.BackupRecord, error) {
if limit <= 0 {
limit = 10
}
var items []model.BackupRecord
if err := r.db.WithContext(ctx).Preload("Task").Preload("Task.StorageTarget").Order("started_at desc").Limit(limit).Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormBackupRecordRepository) ListSuccessfulByTask(ctx context.Context, taskID uint) ([]model.BackupRecord, error) {
var items []model.BackupRecord
if err := r.db.WithContext(ctx).Where("task_id = ? AND status = ?", taskID, "success").Order("completed_at desc, id desc").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormBackupRecordRepository) Count(ctx context.Context) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (r *GormBackupRecordRepository) CountSince(ctx context.Context, since time.Time) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Where("started_at >= ?", since.UTC()).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (r *GormBackupRecordRepository) CountSuccessSince(ctx context.Context, since time.Time) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Where("started_at >= ? AND status = ?", since.UTC(), "success").Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (r *GormBackupRecordRepository) SumFileSize(ctx context.Context) (int64, error) {
var sum int64
if err := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Select("COALESCE(SUM(file_size), 0)").Scan(&sum).Error; err != nil {
return 0, err
}
return sum, nil
}
func (r *GormBackupRecordRepository) TimelineSince(ctx context.Context, since time.Time) ([]BackupTimelinePoint, error) {
var items []BackupTimelinePoint
query := `
SELECT
strftime('%Y-%m-%d', started_at) AS date,
COUNT(*) AS total,
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) AS success,
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) AS failed
FROM backup_records
WHERE started_at >= ?
GROUP BY strftime('%Y-%m-%d', started_at)
ORDER BY date ASC
`
if err := r.db.WithContext(ctx).Raw(query, since.UTC()).Scan(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormBackupRecordRepository) StorageUsage(ctx context.Context) ([]BackupStorageUsageItem, error) {
var items []BackupStorageUsageItem
if err := r.db.WithContext(ctx).Model(&model.BackupRecord{}).Select("storage_target_id, COALESCE(SUM(file_size), 0) AS total_size").Group("storage_target_id").Order("storage_target_id asc").Scan(&items).Error; err != nil {
return nil, err
}
return items, nil
}

View File

@@ -0,0 +1,115 @@
package repository
import (
"context"
"path/filepath"
"testing"
"time"
"backupx/server/internal/config"
"backupx/server/internal/database"
"backupx/server/internal/logger"
"backupx/server/internal/model"
)
func newBackupRecordTestRepository(t *testing.T) *GormBackupRecordRepository {
t.Helper()
log, err := logger.New(config.LogConfig{Level: "error"})
if err != nil {
t.Fatalf("logger.New returned error: %v", err)
}
db, err := database.Open(config.DatabaseConfig{Path: filepath.Join(t.TempDir(), "backupx.db")}, log)
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
storageTarget := &model.StorageTarget{Name: "local", Type: "local_disk", Enabled: true, ConfigCiphertext: "{}", ConfigVersion: 1, LastTestStatus: "unknown"}
if err := db.Create(storageTarget).Error; err != nil {
t.Fatalf("seed storage target error: %v", err)
}
task := &model.BackupTask{Name: "website", Type: "file", Enabled: true, SourcePath: "/srv/www/site", StorageTargetID: storageTarget.ID, RetentionDays: 30, Compression: "gzip", MaxBackups: 10, LastStatus: "idle"}
if err := db.Create(task).Error; err != nil {
t.Fatalf("seed backup task error: %v", err)
}
return NewBackupRecordRepository(db)
}
func TestBackupRecordRepositoryQueries(t *testing.T) {
ctx := context.Background()
repo := newBackupRecordTestRepository(t)
now := time.Now().UTC()
completedAt := now.Add(2 * time.Minute)
record := &model.BackupRecord{
TaskID: 1,
StorageTargetID: 1,
Status: "success",
FileName: "website.tar.gz",
FileSize: 1024,
StoragePath: "tasks/1/website.tar.gz",
DurationSeconds: 120,
LogContent: "done",
StartedAt: now,
CompletedAt: &completedAt,
}
if err := repo.Create(ctx, record); err != nil {
t.Fatalf("Create returned error: %v", err)
}
stored, err := repo.FindByID(ctx, record.ID)
if err != nil {
t.Fatalf("FindByID returned error: %v", err)
}
if stored == nil || stored.FileName != "website.tar.gz" {
t.Fatalf("unexpected stored record: %#v", stored)
}
listed, err := repo.List(ctx, BackupRecordListOptions{TaskID: &record.TaskID, Status: "success"})
if err != nil {
t.Fatalf("List returned error: %v", err)
}
if len(listed) != 1 {
t.Fatalf("expected one listed record, got %d", len(listed))
}
recent, err := repo.ListRecent(ctx, 5)
if err != nil {
t.Fatalf("ListRecent returned error: %v", err)
}
if len(recent) != 1 {
t.Fatalf("expected one recent record, got %d", len(recent))
}
total, err := repo.Count(ctx)
if err != nil {
t.Fatalf("Count returned error: %v", err)
}
if total != 1 {
t.Fatalf("expected total count 1, got %d", total)
}
successCount, err := repo.CountSuccessSince(ctx, now.Add(-time.Hour))
if err != nil {
t.Fatalf("CountSuccessSince returned error: %v", err)
}
if successCount != 1 {
t.Fatalf("expected success count 1, got %d", successCount)
}
sum, err := repo.SumFileSize(ctx)
if err != nil {
t.Fatalf("SumFileSize returned error: %v", err)
}
if sum != 1024 {
t.Fatalf("expected file size sum 1024, got %d", sum)
}
timeline, err := repo.TimelineSince(ctx, now.Add(-time.Hour))
if err != nil {
t.Fatalf("TimelineSince returned error: %v", err)
}
if len(timeline) != 1 || timeline[0].Success != 1 {
t.Fatalf("unexpected timeline: %#v", timeline)
}
usage, err := repo.StorageUsage(ctx)
if err != nil {
t.Fatalf("StorageUsage returned error: %v", err)
}
if len(usage) != 1 || usage[0].TotalSize != 1024 {
t.Fatalf("unexpected usage: %#v", usage)
}
if err := repo.Delete(ctx, record.ID); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
}

View File

@@ -0,0 +1,116 @@
package repository
import (
"context"
"errors"
"backupx/server/internal/model"
"gorm.io/gorm"
)
type BackupTaskListOptions struct {
Type string
Enabled *bool
}
type BackupTaskRepository interface {
List(context.Context, BackupTaskListOptions) ([]model.BackupTask, error)
FindByID(context.Context, uint) (*model.BackupTask, error)
FindByName(context.Context, string) (*model.BackupTask, error)
ListSchedulable(context.Context) ([]model.BackupTask, error)
Count(context.Context) (int64, error)
CountEnabled(context.Context) (int64, error)
CountByStorageTargetID(context.Context, uint) (int64, error)
Create(context.Context, *model.BackupTask) error
Update(context.Context, *model.BackupTask) error
Delete(context.Context, uint) error
}
type GormBackupTaskRepository struct {
db *gorm.DB
}
func NewBackupTaskRepository(db *gorm.DB) *GormBackupTaskRepository {
return &GormBackupTaskRepository{db: db}
}
func (r *GormBackupTaskRepository) List(ctx context.Context, options BackupTaskListOptions) ([]model.BackupTask, error) {
query := r.db.WithContext(ctx).Model(&model.BackupTask{}).Preload("StorageTarget").Order("updated_at desc")
if options.Type != "" {
query = query.Where("type = ?", options.Type)
}
if options.Enabled != nil {
query = query.Where("enabled = ?", *options.Enabled)
}
var items []model.BackupTask
if err := query.Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormBackupTaskRepository) FindByID(ctx context.Context, id uint) (*model.BackupTask, error) {
var item model.BackupTask
if err := r.db.WithContext(ctx).Preload("StorageTarget").First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormBackupTaskRepository) FindByName(ctx context.Context, name string) (*model.BackupTask, error) {
var item model.BackupTask
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormBackupTaskRepository) ListSchedulable(ctx context.Context) ([]model.BackupTask, error) {
var items []model.BackupTask
if err := r.db.WithContext(ctx).Preload("StorageTarget").Where("enabled = ? AND cron_expr <> ''", true).Order("id asc").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormBackupTaskRepository) Count(ctx context.Context) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.BackupTask{}).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (r *GormBackupTaskRepository) CountEnabled(ctx context.Context) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.BackupTask{}).Where("enabled = ?", true).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (r *GormBackupTaskRepository) CountByStorageTargetID(ctx context.Context, storageTargetID uint) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.BackupTask{}).Where("storage_target_id = ?", storageTargetID).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (r *GormBackupTaskRepository) Create(ctx context.Context, item *model.BackupTask) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *GormBackupTaskRepository) Update(ctx context.Context, item *model.BackupTask) error {
return r.db.WithContext(ctx).Save(item).Error
}
func (r *GormBackupTaskRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.BackupTask{}, id).Error
}

View File

@@ -0,0 +1,94 @@
package repository
import (
"context"
"path/filepath"
"testing"
"backupx/server/internal/config"
"backupx/server/internal/database"
"backupx/server/internal/logger"
"backupx/server/internal/model"
)
func newBackupTaskTestRepository(t *testing.T) *GormBackupTaskRepository {
t.Helper()
log, err := logger.New(config.LogConfig{Level: "error"})
if err != nil {
t.Fatalf("logger.New returned error: %v", err)
}
db, err := database.Open(config.DatabaseConfig{Path: filepath.Join(t.TempDir(), "backupx.db")}, log)
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
if err := db.Create(&model.StorageTarget{Name: "local", Type: "local_disk", Enabled: true, ConfigCiphertext: "{}", ConfigVersion: 1, LastTestStatus: "unknown"}).Error; err != nil {
t.Fatalf("seed storage target error: %v", err)
}
return NewBackupTaskRepository(db)
}
func TestBackupTaskRepositoryCRUD(t *testing.T) {
ctx := context.Background()
repo := newBackupTaskTestRepository(t)
task := &model.BackupTask{
Name: "website",
Type: "file",
Enabled: true,
SourcePath: "/srv/www/site",
StorageTargetID: 1,
RetentionDays: 30,
Compression: "gzip",
MaxBackups: 10,
LastStatus: "idle",
}
if err := repo.Create(ctx, task); err != nil {
t.Fatalf("Create returned error: %v", err)
}
stored, err := repo.FindByID(ctx, task.ID)
if err != nil {
t.Fatalf("FindByID returned error: %v", err)
}
if stored == nil || stored.Name != "website" {
t.Fatalf("unexpected stored task: %#v", stored)
}
stored.Enabled = false
stored.CronExpr = "0 3 * * *"
if err := repo.Update(ctx, stored); err != nil {
t.Fatalf("Update returned error: %v", err)
}
schedulable, err := repo.ListSchedulable(ctx)
if err != nil {
t.Fatalf("ListSchedulable returned error: %v", err)
}
if len(schedulable) != 0 {
t.Fatalf("expected disabled task not schedulable, got %d", len(schedulable))
}
stored.Enabled = true
if err := repo.Update(ctx, stored); err != nil {
t.Fatalf("Update returned error: %v", err)
}
schedulable, err = repo.ListSchedulable(ctx)
if err != nil {
t.Fatalf("ListSchedulable returned error: %v", err)
}
if len(schedulable) != 1 {
t.Fatalf("expected one schedulable task, got %d", len(schedulable))
}
count, err := repo.CountByStorageTargetID(ctx, 1)
if err != nil {
t.Fatalf("CountByStorageTargetID returned error: %v", err)
}
if count != 1 {
t.Fatalf("expected referenced task count 1, got %d", count)
}
if err := repo.Delete(ctx, task.ID); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
deleted, err := repo.FindByID(ctx, task.ID)
if err != nil {
t.Fatalf("FindByID after delete returned error: %v", err)
}
if deleted != nil {
t.Fatalf("expected task deleted, got %#v", deleted)
}
}

View File

@@ -0,0 +1,80 @@
package repository
import (
"context"
"errors"
"backupx/server/internal/model"
"gorm.io/gorm"
)
type NodeRepository interface {
List(context.Context) ([]model.Node, error)
FindByID(context.Context, uint) (*model.Node, error)
FindByToken(context.Context, string) (*model.Node, error)
FindLocal(context.Context) (*model.Node, error)
Create(context.Context, *model.Node) error
Update(context.Context, *model.Node) error
Delete(context.Context, uint) error
}
type GormNodeRepository struct {
db *gorm.DB
}
func NewNodeRepository(db *gorm.DB) *GormNodeRepository {
return &GormNodeRepository{db: db}
}
func (r *GormNodeRepository) List(ctx context.Context) ([]model.Node, error) {
var items []model.Node
if err := r.db.WithContext(ctx).Order("is_local desc, updated_at desc").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormNodeRepository) FindByID(ctx context.Context, id uint) (*model.Node, error) {
var item model.Node
if err := r.db.WithContext(ctx).First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormNodeRepository) FindByToken(ctx context.Context, token string) (*model.Node, error) {
var item model.Node
if err := r.db.WithContext(ctx).Where("token = ?", token).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormNodeRepository) FindLocal(ctx context.Context) (*model.Node, error) {
var item model.Node
if err := r.db.WithContext(ctx).Where("is_local = ?", true).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormNodeRepository) Create(ctx context.Context, item *model.Node) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *GormNodeRepository) Update(ctx context.Context, item *model.Node) error {
return r.db.WithContext(ctx).Save(item).Error
}
func (r *GormNodeRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.Node{}, id).Error
}

View File

@@ -0,0 +1,83 @@
package repository
import (
"context"
"errors"
"backupx/server/internal/model"
"gorm.io/gorm"
)
type NotificationRepository interface {
List(context.Context) ([]model.Notification, error)
ListEnabledForEvent(context.Context, bool) ([]model.Notification, error)
FindByID(context.Context, uint) (*model.Notification, error)
FindByName(context.Context, string) (*model.Notification, error)
Create(context.Context, *model.Notification) error
Update(context.Context, *model.Notification) error
Delete(context.Context, uint) error
}
type GormNotificationRepository struct {
db *gorm.DB
}
func NewNotificationRepository(db *gorm.DB) *GormNotificationRepository {
return &GormNotificationRepository{db: db}
}
func (r *GormNotificationRepository) List(ctx context.Context) ([]model.Notification, error) {
var items []model.Notification
if err := r.db.WithContext(ctx).Order("updated_at desc").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormNotificationRepository) ListEnabledForEvent(ctx context.Context, success bool) ([]model.Notification, error) {
query := r.db.WithContext(ctx).Model(&model.Notification{}).Where("enabled = ?", true)
if success {
query = query.Where("on_success = ?", true)
} else {
query = query.Where("on_failure = ?", true)
}
var items []model.Notification
if err := query.Order("updated_at desc").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormNotificationRepository) FindByID(ctx context.Context, id uint) (*model.Notification, error) {
var item model.Notification
if err := r.db.WithContext(ctx).First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormNotificationRepository) FindByName(ctx context.Context, name string) (*model.Notification, error) {
var item model.Notification
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormNotificationRepository) Create(ctx context.Context, item *model.Notification) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *GormNotificationRepository) Update(ctx context.Context, item *model.Notification) error {
return r.db.WithContext(ctx).Save(item).Error
}
func (r *GormNotificationRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.Notification{}, id).Error
}

View File

@@ -0,0 +1,69 @@
package repository
import (
"context"
"path/filepath"
"testing"
"backupx/server/internal/config"
"backupx/server/internal/database"
"backupx/server/internal/logger"
"backupx/server/internal/model"
)
func newNotificationTestRepository(t *testing.T) *GormNotificationRepository {
t.Helper()
log, err := logger.New(config.LogConfig{Level: "error"})
if err != nil {
t.Fatalf("logger.New returned error: %v", err)
}
db, err := database.Open(config.DatabaseConfig{Path: filepath.Join(t.TempDir(), "backupx.db")}, log)
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
return NewNotificationRepository(db)
}
func TestNotificationRepositoryCRUD(t *testing.T) {
ctx := context.Background()
repo := newNotificationTestRepository(t)
item := &model.Notification{
Type: "webhook",
Name: "ops-webhook",
ConfigCiphertext: "ciphertext",
Enabled: true,
OnSuccess: false,
OnFailure: true,
}
if err := repo.Create(ctx, item); err != nil {
t.Fatalf("Create returned error: %v", err)
}
stored, err := repo.FindByName(ctx, "ops-webhook")
if err != nil {
t.Fatalf("FindByName returned error: %v", err)
}
if stored == nil || stored.Name != "ops-webhook" {
t.Fatalf("unexpected notification: %#v", stored)
}
enabledForFailure, err := repo.ListEnabledForEvent(ctx, false)
if err != nil {
t.Fatalf("ListEnabledForEvent returned error: %v", err)
}
if len(enabledForFailure) != 1 {
t.Fatalf("expected one failure notification, got %d", len(enabledForFailure))
}
stored.OnSuccess = true
if err := repo.Update(ctx, stored); err != nil {
t.Fatalf("Update returned error: %v", err)
}
enabledForSuccess, err := repo.ListEnabledForEvent(ctx, true)
if err != nil {
t.Fatalf("ListEnabledForEvent returned error: %v", err)
}
if len(enabledForSuccess) != 1 {
t.Fatalf("expected one success notification, got %d", len(enabledForSuccess))
}
if err := repo.Delete(ctx, item.ID); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
}

View File

@@ -0,0 +1,48 @@
package repository
import (
"context"
"errors"
"time"
"backupx/server/internal/model"
"gorm.io/gorm"
)
type OAuthSessionRepository interface {
Create(context.Context, *model.OAuthSession) error
Update(context.Context, *model.OAuthSession) error
FindByState(context.Context, string) (*model.OAuthSession, error)
DeleteExpired(context.Context, time.Time) error
}
type GormOAuthSessionRepository struct {
db *gorm.DB
}
func NewOAuthSessionRepository(db *gorm.DB) *GormOAuthSessionRepository {
return &GormOAuthSessionRepository{db: db}
}
func (r *GormOAuthSessionRepository) Create(ctx context.Context, item *model.OAuthSession) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *GormOAuthSessionRepository) Update(ctx context.Context, item *model.OAuthSession) error {
return r.db.WithContext(ctx).Save(item).Error
}
func (r *GormOAuthSessionRepository) FindByState(ctx context.Context, state string) (*model.OAuthSession, error) {
var item model.OAuthSession
if err := r.db.WithContext(ctx).Where("state = ?", state).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormOAuthSessionRepository) DeleteExpired(ctx context.Context, before time.Time) error {
return r.db.WithContext(ctx).Where("expires_at <= ?", before).Delete(&model.OAuthSession{}).Error
}

View File

@@ -0,0 +1,73 @@
package repository
import (
"context"
"path/filepath"
"testing"
"time"
"backupx/server/internal/config"
"backupx/server/internal/database"
"backupx/server/internal/logger"
"backupx/server/internal/model"
)
func newOAuthSessionTestRepository(t *testing.T) *GormOAuthSessionRepository {
t.Helper()
log, err := logger.New(config.LogConfig{Level: "error"})
if err != nil {
t.Fatalf("logger.New returned error: %v", err)
}
db, err := database.Open(config.DatabaseConfig{Path: filepath.Join(t.TempDir(), "backupx.db")}, log)
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
return NewOAuthSessionRepository(db)
}
func TestOAuthSessionRepositoryCRUDAndDeleteExpired(t *testing.T) {
ctx := context.Background()
repo := newOAuthSessionTestRepository(t)
expiresAt := time.Now().UTC().Add(5 * time.Minute)
session := &model.OAuthSession{
ProviderType: "google_drive",
State: "oauth-state",
PayloadCiphertext: "ciphertext",
ExpiresAt: expiresAt,
}
if err := repo.Create(ctx, session); err != nil {
t.Fatalf("Create returned error: %v", err)
}
stored, err := repo.FindByState(ctx, "oauth-state")
if err != nil {
t.Fatalf("FindByState returned error: %v", err)
}
if stored == nil || stored.State != "oauth-state" {
t.Fatalf("unexpected stored session: %#v", stored)
}
now := time.Now().UTC()
stored.UsedAt = &now
if err := repo.Update(ctx, stored); err != nil {
t.Fatalf("Update returned error: %v", err)
}
if err := repo.DeleteExpired(ctx, time.Now().UTC().Add(-time.Minute)); err != nil {
t.Fatalf("DeleteExpired returned error: %v", err)
}
stillThere, err := repo.FindByState(ctx, "oauth-state")
if err != nil {
t.Fatalf("FindByState after DeleteExpired returned error: %v", err)
}
if stillThere == nil {
t.Fatalf("expected unexpired session to remain")
}
if err := repo.DeleteExpired(ctx, time.Now().UTC().Add(10*time.Minute)); err != nil {
t.Fatalf("DeleteExpired returned error: %v", err)
}
deleted, err := repo.FindByState(ctx, "oauth-state")
if err != nil {
t.Fatalf("FindByState after expiration delete returned error: %v", err)
}
if deleted != nil {
t.Fatalf("expected session to be deleted, got %#v", deleted)
}
}

View File

@@ -0,0 +1,68 @@
package repository
import (
"context"
"errors"
"backupx/server/internal/model"
"gorm.io/gorm"
)
type StorageTargetRepository interface {
List(context.Context) ([]model.StorageTarget, error)
FindByID(context.Context, uint) (*model.StorageTarget, error)
FindByName(context.Context, string) (*model.StorageTarget, error)
Create(context.Context, *model.StorageTarget) error
Update(context.Context, *model.StorageTarget) error
Delete(context.Context, uint) error
}
type GormStorageTargetRepository struct {
db *gorm.DB
}
func NewStorageTargetRepository(db *gorm.DB) *GormStorageTargetRepository {
return &GormStorageTargetRepository{db: db}
}
func (r *GormStorageTargetRepository) List(ctx context.Context) ([]model.StorageTarget, error) {
var items []model.StorageTarget
if err := r.db.WithContext(ctx).Order("updated_at desc").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormStorageTargetRepository) FindByID(ctx context.Context, id uint) (*model.StorageTarget, error) {
var item model.StorageTarget
if err := r.db.WithContext(ctx).First(&item, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormStorageTargetRepository) FindByName(ctx context.Context, name string) (*model.StorageTarget, error) {
var item model.StorageTarget
if err := r.db.WithContext(ctx).Where("name = ?", name).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormStorageTargetRepository) Create(ctx context.Context, item *model.StorageTarget) error {
return r.db.WithContext(ctx).Create(item).Error
}
func (r *GormStorageTargetRepository) Update(ctx context.Context, item *model.StorageTarget) error {
return r.db.WithContext(ctx).Save(item).Error
}
func (r *GormStorageTargetRepository) Delete(ctx context.Context, id uint) error {
return r.db.WithContext(ctx).Delete(&model.StorageTarget{}, id).Error
}

View File

@@ -0,0 +1,81 @@
package repository
import (
"context"
"path/filepath"
"testing"
"backupx/server/internal/config"
"backupx/server/internal/database"
"backupx/server/internal/logger"
"backupx/server/internal/model"
)
func openTestDB(t *testing.T) context.Context {
t.Helper()
return context.Background()
}
func newStorageTestRepository(t *testing.T) *GormStorageTargetRepository {
t.Helper()
log, err := logger.New(config.LogConfig{Level: "error"})
if err != nil {
t.Fatalf("logger.New returned error: %v", err)
}
db, err := database.Open(config.DatabaseConfig{Path: filepath.Join(t.TempDir(), "backupx.db")}, log)
if err != nil {
t.Fatalf("database.Open returned error: %v", err)
}
return NewStorageTargetRepository(db)
}
func TestStorageTargetRepositoryCRUD(t *testing.T) {
ctx := openTestDB(t)
repo := newStorageTestRepository(t)
item := &model.StorageTarget{
Name: "local",
Type: "local_disk",
Enabled: true,
ConfigCiphertext: "ciphertext",
ConfigVersion: 1,
LastTestStatus: "unknown",
}
if err := repo.Create(ctx, item); err != nil {
t.Fatalf("Create returned error: %v", err)
}
stored, err := repo.FindByID(ctx, item.ID)
if err != nil {
t.Fatalf("FindByID returned error: %v", err)
}
if stored == nil || stored.Name != "local" {
t.Fatalf("unexpected stored target: %#v", stored)
}
byName, err := repo.FindByName(ctx, "local")
if err != nil {
t.Fatalf("FindByName returned error: %v", err)
}
if byName == nil || byName.ID != item.ID {
t.Fatalf("expected target lookup by name to match, got %#v", byName)
}
stored.Description = "updated"
if err := repo.Update(ctx, stored); err != nil {
t.Fatalf("Update returned error: %v", err)
}
items, err := repo.List(ctx)
if err != nil {
t.Fatalf("List returned error: %v", err)
}
if len(items) != 1 || items[0].Description != "updated" {
t.Fatalf("unexpected list result: %#v", items)
}
if err := repo.Delete(ctx, item.ID); err != nil {
t.Fatalf("Delete returned error: %v", err)
}
deleted, err := repo.FindByID(ctx, item.ID)
if err != nil {
t.Fatalf("FindByID after delete returned error: %v", err)
}
if deleted != nil {
t.Fatalf("expected target to be deleted, got %#v", deleted)
}
}

View File

@@ -0,0 +1,50 @@
package repository
import (
"context"
"errors"
"backupx/server/internal/model"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type SystemConfigRepository interface {
GetByKey(context.Context, string) (*model.SystemConfig, error)
List(context.Context) ([]model.SystemConfig, error)
Upsert(context.Context, *model.SystemConfig) error
}
type GormSystemConfigRepository struct {
db *gorm.DB
}
func NewSystemConfigRepository(db *gorm.DB) *GormSystemConfigRepository {
return &GormSystemConfigRepository{db: db}
}
func (r *GormSystemConfigRepository) GetByKey(ctx context.Context, key string) (*model.SystemConfig, error) {
var item model.SystemConfig
if err := r.db.WithContext(ctx).Where("key = ?", key).First(&item).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &item, nil
}
func (r *GormSystemConfigRepository) List(ctx context.Context) ([]model.SystemConfig, error) {
var items []model.SystemConfig
if err := r.db.WithContext(ctx).Order("key ASC").Find(&items).Error; err != nil {
return nil, err
}
return items, nil
}
func (r *GormSystemConfigRepository) Upsert(ctx context.Context, item *model.SystemConfig) error {
return r.db.WithContext(ctx).Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "key"}},
DoUpdates: clause.AssignmentColumns([]string{"value", "encrypted", "updated_at"}),
}).Create(item).Error
}

View File

@@ -0,0 +1,63 @@
package repository
import (
"context"
"errors"
"backupx/server/internal/model"
"gorm.io/gorm"
)
type UserRepository interface {
Count(context.Context) (int64, error)
Create(context.Context, *model.User) error
Update(context.Context, *model.User) error
FindByUsername(context.Context, string) (*model.User, error)
FindByID(context.Context, uint) (*model.User, error)
}
type GormUserRepository struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) *GormUserRepository {
return &GormUserRepository{db: db}
}
func (r *GormUserRepository) Count(ctx context.Context) (int64, error) {
var count int64
if err := r.db.WithContext(ctx).Model(&model.User{}).Count(&count).Error; err != nil {
return 0, err
}
return count, nil
}
func (r *GormUserRepository) Create(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Create(user).Error
}
func (r *GormUserRepository) Update(ctx context.Context, user *model.User) error {
return r.db.WithContext(ctx).Save(user).Error
}
func (r *GormUserRepository) FindByUsername(ctx context.Context, username string) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}
func (r *GormUserRepository) FindByID(ctx context.Context, id uint) (*model.User, error) {
var user model.User
if err := r.db.WithContext(ctx).First(&user, id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, nil
}
return nil, err
}
return &user, nil
}