refac: decouple persistence and image storage behind driver registries
This commit is contained in:
1 parent
421cfafec3
commit
81e299f09a
46 files changed
+1036
-374
No files matched your search
@@ -0,0 +1,52 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUpdate "RenewCMS/internal/domain/update"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Driver string
|
||||
Source string
|
||||
}
|
||||
|
||||
type Driver interface {
|
||||
Open(source string) (*gorm.DB, error)
|
||||
NewBackup(db *gorm.DB, directory string, now func() time.Time) domainUpdate.DatabaseBackup
|
||||
}
|
||||
|
||||
var drivers = map[string]Driver{
|
||||
DriverSQLite: sqliteDriver{},
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
DB *gorm.DB
|
||||
driver Driver
|
||||
}
|
||||
|
||||
func Connect(config Config) (*Connection, error) {
|
||||
driver, ok := drivers[config.Driver]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported database driver %q", config.Driver)
|
||||
}
|
||||
db, err := driver.Open(config.Source)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Connection{DB: db, driver: driver}, nil
|
||||
}
|
||||
|
||||
func (c *Connection) NewBackup(directory string, now func() time.Time) domainUpdate.DatabaseBackup {
|
||||
return c.driver.NewBackup(c.DB, directory, now)
|
||||
}
|
||||
|
||||
func (c *Connection) Close() error {
|
||||
sqlDb, err := c.DB.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDb.Close()
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func Migrate(db *gorm.DB) error {
|
||||
err := db.AutoMigrate(&models.Article{}, &models.Image{}, &models.User{}, &models.Invite{}, &models.APIKey{}, &models.PurgeSettings{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return BackfillUserRoles(db)
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUpdate "RenewCMS/internal/domain/update"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const DriverSQLite = "sqlite"
|
||||
|
||||
const sqliteBusyTimeoutMs = 5000
|
||||
|
||||
type sqliteDriver struct{}
|
||||
|
||||
func SQLiteDSN(dbName string) string {
|
||||
return fmt.Sprintf("%s?_pragma=busy_timeout(%d)", dbName, sqliteBusyTimeoutMs)
|
||||
}
|
||||
|
||||
func (sqliteDriver) Open(file string) (*gorm.DB, error) {
|
||||
if err := os.MkdirAll(filepath.Dir(file), os.ModePerm); err != nil {
|
||||
return nil, fmt.Errorf("unable to create necessary subdirectories: %w", err)
|
||||
}
|
||||
return gorm.Open(sqlite.Open(SQLiteDSN(file)), &gorm.Config{})
|
||||
}
|
||||
|
||||
func (sqliteDriver) NewBackup(db *gorm.DB, directory string, now func() time.Time) domainUpdate.DatabaseBackup {
|
||||
return NewSQLiteBackup(db, directory, now)
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domain "RenewCMS/internal/domain/update"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var errBackupExists = errors.New("a backup with this name already exists")
|
||||
|
||||
const (
|
||||
backupsToKeep = 3
|
||||
backupFileExtension = ".db"
|
||||
backupFilePrefix = "renewcms-"
|
||||
backupTimeLayout = "20060102T150405.000000000Z"
|
||||
)
|
||||
|
||||
type SQLiteBackup struct {
|
||||
db *gorm.DB
|
||||
directory string
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
var _ domain.DatabaseBackup = &SQLiteBackup{}
|
||||
|
||||
func NewSQLiteBackup(db *gorm.DB, directory string, now func() time.Time) *SQLiteBackup {
|
||||
return &SQLiteBackup{db: db, directory: directory, now: now}
|
||||
}
|
||||
|
||||
func (b *SQLiteBackup) Backup() (string, error) {
|
||||
if err := os.MkdirAll(b.directory, os.ModePerm); err != nil {
|
||||
return "", err
|
||||
}
|
||||
target := filepath.Join(b.directory, backupFilePrefix+b.now().UTC().Format(backupTimeLayout)+backupFileExtension)
|
||||
if _, err := os.Stat(target); err == nil {
|
||||
return "", errBackupExists
|
||||
}
|
||||
if err := b.db.Exec("VACUUM INTO ?", target).Error; err != nil {
|
||||
_ = os.Remove(target)
|
||||
return "", err
|
||||
}
|
||||
if err := b.prune(); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
type backupFile struct {
|
||||
path string
|
||||
modified time.Time
|
||||
}
|
||||
|
||||
func (b *SQLiteBackup) prune() error {
|
||||
entries, err := os.ReadDir(b.directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files := make([]backupFile, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !strings.HasSuffix(entry.Name(), backupFileExtension) {
|
||||
continue
|
||||
}
|
||||
info, err := entry.Info()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
files = append(files, backupFile{path: filepath.Join(b.directory, entry.Name()), modified: info.ModTime()})
|
||||
}
|
||||
sort.Slice(files, func(i, j int) bool {
|
||||
if files[i].modified.Equal(files[j].modified) {
|
||||
return files[i].path > files[j].path
|
||||
}
|
||||
return files[i].modified.After(files[j].modified)
|
||||
})
|
||||
for _, stale := range files[min(backupsToKeep, len(files)):] {
|
||||
if err := os.Remove(stale.path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
domainUser "RenewCMS/internal/domain/user"
|
||||
entity "RenewCMS/internal/infrastructure/persistence/models"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func BackfillUserRoles(db *gorm.DB) error {
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var firstUser entity.User
|
||||
result := tx.Where("role = ''").Order("id asc").Limit(1).Find(&firstUser)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var lowest entity.User
|
||||
if err := tx.Order("id asc").First(&lowest).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if lowest.Role == "" {
|
||||
if err := tx.Model(&entity.User{}).Where("id = ?", lowest.ID).Update("role", string(domainUser.RoleOwner)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return tx.Model(&entity.User{}).Where("role = ''").Update("role", string(domainUser.RoleAdmin)).Error
|
||||
})
|
||||
}
|
||||
Reference in new issue
Block a user