mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-03 19:06:04 -04:00
Automatic pre-migration DB backups
This commit is contained in:
parent
aa159ac453
commit
5a58d24156
@ -30,7 +30,7 @@ type UserError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *UserError) Error() string {
|
||||
func (e UserError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
|
@ -81,6 +81,7 @@ type Config struct {
|
||||
ListenAddress string
|
||||
LogRequests bool
|
||||
MinPasswordLength int
|
||||
PreMigrationBackups bool
|
||||
RateLimit rateLimitConfig
|
||||
RegistrationExistingPlayer registrationExistingPlayerConfig
|
||||
RegistrationNewPlayer registrationNewPlayerConfig
|
||||
@ -128,6 +129,7 @@ func DefaultConfig() Config {
|
||||
LogRequests: true,
|
||||
MinPasswordLength: 8,
|
||||
OfflineSkins: true,
|
||||
PreMigrationBackups: true,
|
||||
RateLimit: defaultRateLimitConfig,
|
||||
RegistrationExistingPlayer: registrationExistingPlayerConfig{
|
||||
Allow: false,
|
||||
|
39
db.go
39
db.go
@ -5,12 +5,14 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
mapset "github.com/deckarep/golang-set/v2"
|
||||
"github.com/samber/mo"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -47,6 +49,14 @@ func IsErrorPlayerNameTakenByUsername(err error) bool {
|
||||
return err.Error() == PLAYER_NAME_TAKEN_BY_USERNAME_ERROR
|
||||
}
|
||||
|
||||
type BackwardsMigrationError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e BackwardsMigrationError) Error() string {
|
||||
return e.Err.Error()
|
||||
}
|
||||
|
||||
type V1User struct {
|
||||
IsAdmin bool
|
||||
IsLocked bool
|
||||
@ -137,7 +147,7 @@ func OpenDB(config *Config) (*gorm.DB, error) {
|
||||
db := Unwrap(gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
}))
|
||||
err = Migrate(config, db, alreadyExisted, CURRENT_USER_VERSION)
|
||||
err = Migrate(config, mo.Some(dbPath), db, alreadyExisted, CURRENT_USER_VERSION)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Error migrating database: %w", err)
|
||||
}
|
||||
@ -150,7 +160,7 @@ func setUserVersion(tx *gorm.DB, userVersion uint) error {
|
||||
return tx.Exec(fmt.Sprintf("PRAGMA user_version = %d", userVersion)).Error
|
||||
}
|
||||
|
||||
func Migrate(config *Config, db *gorm.DB, alreadyExisted bool, targetUserVersion uint) error {
|
||||
func Migrate(config *Config, dbPath mo.Option[string], db *gorm.DB, alreadyExisted bool, targetUserVersion uint) error {
|
||||
var userVersion uint
|
||||
|
||||
if alreadyExisted {
|
||||
@ -162,10 +172,29 @@ func Migrate(config *Config, db *gorm.DB, alreadyExisted bool, targetUserVersion
|
||||
}
|
||||
|
||||
initialUserVersion := userVersion
|
||||
if initialUserVersion > targetUserVersion {
|
||||
return BackwardsMigrationError{
|
||||
Err: fmt.Errorf("Database is version %d, migration target version is %d, cannot continue. Are you trying to run an older version of %s with a newer database?", userVersion, targetUserVersion, config.ApplicationName),
|
||||
}
|
||||
}
|
||||
|
||||
if initialUserVersion < targetUserVersion {
|
||||
log.Printf("Started migration of database version %d to %d", userVersion, targetUserVersion)
|
||||
} else if initialUserVersion > targetUserVersion {
|
||||
return fmt.Errorf("Database is version %d, migration target version is %d, cannot continue. Are you trying to run an older version of %s with a newer database?", userVersion, targetUserVersion, config.ApplicationName)
|
||||
log.Printf("Started migration of database version %d to %d.", userVersion, targetUserVersion)
|
||||
if !config.PreMigrationBackups {
|
||||
log.Printf("PreMigrationBackups disabled, skipping backup.")
|
||||
} else if p, ok := dbPath.Get(); ok {
|
||||
dbDir := filepath.Dir(p)
|
||||
datetime := time.Now().UTC().Format("2006-01-02T15-04-05Z")
|
||||
backupPath := path.Join(dbDir, fmt.Sprintf("drasl.%d.%s.db", userVersion, datetime))
|
||||
log.Printf("Backing up old database to %s", backupPath)
|
||||
_, err := CopyPath(p, backupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error backing up database: %w", err)
|
||||
}
|
||||
log.Printf("Database backed up, proceeding.")
|
||||
} else {
|
||||
log.Printf("Database path not specified, skipping backup.")
|
||||
}
|
||||
}
|
||||
|
||||
err := db.Transaction(func(tx *gorm.DB) error {
|
||||
|
33
db_test.go
33
db_test.go
@ -1,6 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/samber/mo"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
@ -8,16 +10,11 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func (ts *TestSuite) getFreshDatabase(t *testing.T) *gorm.DB {
|
||||
dbPath := path.Join(ts.Config.StateDirectory, "drasl.db")
|
||||
if err := os.Remove(dbPath); err != nil {
|
||||
assert.True(t, os.IsNotExist(err))
|
||||
}
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
assert.Nil(t, err)
|
||||
@ -45,11 +42,12 @@ func TestDB(t *testing.T) {
|
||||
t.Run("Test 2->3 migration", ts.testMigrate2To3)
|
||||
t.Run("Test 3->4 migration", ts.testMigrate3To4)
|
||||
t.Run("Test 3->4 migration, username/player name collision", ts.testMigrate3To4Collision)
|
||||
t.Run("Test backwards migration", ts.testMigrateBackwards)
|
||||
}
|
||||
|
||||
func (ts *TestSuite) testFreshDatabase(t *testing.T) {
|
||||
db := ts.getFreshDatabase(t)
|
||||
err := Migrate(ts.Config, db, false, CURRENT_USER_VERSION)
|
||||
err := Migrate(ts.Config, mo.None[string](), db, false, CURRENT_USER_VERSION)
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
@ -63,7 +61,7 @@ func (ts *TestSuite) testMigrate1To2(t *testing.T) {
|
||||
var v1Client V1Client
|
||||
assert.Nil(t, db.First(&v1Client).Error)
|
||||
|
||||
err = Migrate(ts.Config, db, true, 2)
|
||||
err = Migrate(ts.Config, mo.None[string](), db, true, 2)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var v2Client V2Client
|
||||
@ -83,7 +81,7 @@ func (ts *TestSuite) testMigrate2To3(t *testing.T) {
|
||||
var v2User V2User
|
||||
assert.Nil(t, db.First(&v2User).Error)
|
||||
|
||||
err = Migrate(ts.Config, db, true, 3)
|
||||
err = Migrate(ts.Config, mo.None[string](), db, true, 3)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var v3User V3User
|
||||
@ -101,7 +99,7 @@ func (ts *TestSuite) testMigrate3To4(t *testing.T) {
|
||||
var v3User V3User
|
||||
assert.Nil(t, db.First(&v3User).Error)
|
||||
|
||||
err = Migrate(ts.Config, db, true, 4)
|
||||
err = Migrate(ts.Config, mo.None[string](), db, true, 4)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var v4User V4User
|
||||
@ -133,7 +131,7 @@ func (ts *TestSuite) testMigrate3To4Collision(t *testing.T) {
|
||||
assert.Nil(t, db.First(&v3qux, "username = ?", "qux").Error)
|
||||
assert.Equal(t, "foo", v3qux.PlayerName)
|
||||
|
||||
err = Migrate(ts.Config, db, true, 4)
|
||||
err = Migrate(ts.Config, mo.None[string](), db, true, 4)
|
||||
assert.Nil(t, err)
|
||||
|
||||
var v4foo V4User
|
||||
@ -146,3 +144,16 @@ func (ts *TestSuite) testMigrate3To4Collision(t *testing.T) {
|
||||
assert.Equal(t, 1, len(v4qux.Players))
|
||||
assert.Equal(t, "qux", v4qux.Players[0].Name)
|
||||
}
|
||||
|
||||
func (ts *TestSuite) testMigrateBackwards(t *testing.T) {
|
||||
db := ts.getFreshDatabase(t)
|
||||
|
||||
query, err := os.ReadFile("sql/1.sql")
|
||||
assert.Nil(t, err)
|
||||
assert.Nil(t, db.Exec(string(query)).Error)
|
||||
setUserVersion(db, CURRENT_USER_VERSION+1)
|
||||
|
||||
err = Migrate(ts.Config, mo.None[string](), db, true, CURRENT_USER_VERSION)
|
||||
var backwardsMigrationError BackwardsMigrationError
|
||||
assert.True(t, errors.As(err, &backwardsMigrationError))
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ Other available options:
|
||||
- `ListenAddress`: IP address and port to listen on. Depending on how you configure your reverse proxy and whether you run Drasl in a container, you should consider setting the listen address to `"127.0.0.1:25585"` to ensure Drasl is only accessible through the reverse proxy. If your reverse proxy is unable to connect to Drasl, try setting this back to the default value. String. Default value: `"0.0.0.0:25585"`.
|
||||
- `DefaultAdmins`: Usernames of the instance's permanent admins. Admin rights can be granted to other accounts using the web UI, but admins defined via `DefaultAdmins` cannot be demoted unless they are removed from the config file. Array of strings. Default value: `[]`.
|
||||
- `DefaultMaxPlayerCount`: Number of players each user is allowed to own by default. Admins can increase or decrease each user's individual limit. Use `-1` to allow creating an unlimited number of players. Integer. Default value: `1`.
|
||||
- `PreMigrationBackups`: Back up the database to `/path/to/StateDirectory/drasl.X.YYYY-mm-ddTHH-MM-SSZ.db` (where `X` is the old database version) before migrating to a new database version. Boolean. Default value: `true`.
|
||||
- `EnableBackgroundEffect`: Whether to enable the 3D background animation in the web UI. Boolean. Default value: `true`.
|
||||
- `EnableFooter`: Whether to enable the page footer in the web UI. Boolean. Default value: `true`.
|
||||
- `[RateLimit]`: Rate-limit requests per IP address to limit abuse. Only applies to certain web UI routes, not any Yggdrasil routes. Requests for skins, capes, and web pages are also unaffected. Uses [Echo](https://echo.labstack.com)'s [rate limiter middleware](https://echo.labstack.com/middleware/rate-limiter/).
|
||||
|
22
util.go
22
util.go
@ -8,7 +8,9 @@ import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"github.com/jxskiss/base62"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@ -141,3 +143,23 @@ func (m *KeyedMutex) Lock(key string) func() {
|
||||
|
||||
return func() { mtx.Unlock() }
|
||||
}
|
||||
|
||||
func CopyPath(sourcePath string, destinationPath string) (int64, error) {
|
||||
source, err := os.Open(sourcePath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer source.Close()
|
||||
|
||||
destination, err := os.Create(destinationPath)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer destination.Close()
|
||||
|
||||
bytesWritten, err := io.Copy(destination, source)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return bytesWritten, nil
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user