mirror of
https://github.com/unmojang/drasl.git
synced 2025-08-04 03:16:05 -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
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *UserError) Error() string {
|
func (e UserError) Error() string {
|
||||||
return e.Err.Error()
|
return e.Err.Error()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ type Config struct {
|
|||||||
ListenAddress string
|
ListenAddress string
|
||||||
LogRequests bool
|
LogRequests bool
|
||||||
MinPasswordLength int
|
MinPasswordLength int
|
||||||
|
PreMigrationBackups bool
|
||||||
RateLimit rateLimitConfig
|
RateLimit rateLimitConfig
|
||||||
RegistrationExistingPlayer registrationExistingPlayerConfig
|
RegistrationExistingPlayer registrationExistingPlayerConfig
|
||||||
RegistrationNewPlayer registrationNewPlayerConfig
|
RegistrationNewPlayer registrationNewPlayerConfig
|
||||||
@ -128,6 +129,7 @@ func DefaultConfig() Config {
|
|||||||
LogRequests: true,
|
LogRequests: true,
|
||||||
MinPasswordLength: 8,
|
MinPasswordLength: 8,
|
||||||
OfflineSkins: true,
|
OfflineSkins: true,
|
||||||
|
PreMigrationBackups: true,
|
||||||
RateLimit: defaultRateLimitConfig,
|
RateLimit: defaultRateLimitConfig,
|
||||||
RegistrationExistingPlayer: registrationExistingPlayerConfig{
|
RegistrationExistingPlayer: registrationExistingPlayerConfig{
|
||||||
Allow: false,
|
Allow: false,
|
||||||
|
39
db.go
39
db.go
@ -5,12 +5,14 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
mapset "github.com/deckarep/golang-set/v2"
|
mapset "github.com/deckarep/golang-set/v2"
|
||||||
|
"github.com/samber/mo"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"gorm.io/gorm/logger"
|
"gorm.io/gorm/logger"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -47,6 +49,14 @@ func IsErrorPlayerNameTakenByUsername(err error) bool {
|
|||||||
return err.Error() == PLAYER_NAME_TAKEN_BY_USERNAME_ERROR
|
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 {
|
type V1User struct {
|
||||||
IsAdmin bool
|
IsAdmin bool
|
||||||
IsLocked bool
|
IsLocked bool
|
||||||
@ -137,7 +147,7 @@ func OpenDB(config *Config) (*gorm.DB, error) {
|
|||||||
db := Unwrap(gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
db := Unwrap(gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Error migrating database: %w", err)
|
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
|
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
|
var userVersion uint
|
||||||
|
|
||||||
if alreadyExisted {
|
if alreadyExisted {
|
||||||
@ -162,10 +172,29 @@ func Migrate(config *Config, db *gorm.DB, alreadyExisted bool, targetUserVersion
|
|||||||
}
|
}
|
||||||
|
|
||||||
initialUserVersion := userVersion
|
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 {
|
if initialUserVersion < targetUserVersion {
|
||||||
log.Printf("Started migration of database version %d to %d", userVersion, targetUserVersion)
|
log.Printf("Started migration of database version %d to %d.", userVersion, targetUserVersion)
|
||||||
} else if initialUserVersion > targetUserVersion {
|
if !config.PreMigrationBackups {
|
||||||
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("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 {
|
err := db.Transaction(func(tx *gorm.DB) error {
|
||||||
|
33
db_test.go
33
db_test.go
@ -1,6 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/samber/mo"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@ -8,16 +10,11 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (ts *TestSuite) getFreshDatabase(t *testing.T) *gorm.DB {
|
func (ts *TestSuite) getFreshDatabase(t *testing.T) *gorm.DB {
|
||||||
dbPath := path.Join(ts.Config.StateDirectory, "drasl.db")
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
||||||
if err := os.Remove(dbPath); err != nil {
|
|
||||||
assert.True(t, os.IsNotExist(err))
|
|
||||||
}
|
|
||||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
|
|
||||||
Logger: logger.Default.LogMode(logger.Silent),
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
})
|
})
|
||||||
assert.Nil(t, err)
|
assert.Nil(t, err)
|
||||||
@ -45,11 +42,12 @@ func TestDB(t *testing.T) {
|
|||||||
t.Run("Test 2->3 migration", ts.testMigrate2To3)
|
t.Run("Test 2->3 migration", ts.testMigrate2To3)
|
||||||
t.Run("Test 3->4 migration", ts.testMigrate3To4)
|
t.Run("Test 3->4 migration", ts.testMigrate3To4)
|
||||||
t.Run("Test 3->4 migration, username/player name collision", ts.testMigrate3To4Collision)
|
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) {
|
func (ts *TestSuite) testFreshDatabase(t *testing.T) {
|
||||||
db := ts.getFreshDatabase(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)
|
assert.Nil(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -63,7 +61,7 @@ func (ts *TestSuite) testMigrate1To2(t *testing.T) {
|
|||||||
var v1Client V1Client
|
var v1Client V1Client
|
||||||
assert.Nil(t, db.First(&v1Client).Error)
|
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)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var v2Client V2Client
|
var v2Client V2Client
|
||||||
@ -83,7 +81,7 @@ func (ts *TestSuite) testMigrate2To3(t *testing.T) {
|
|||||||
var v2User V2User
|
var v2User V2User
|
||||||
assert.Nil(t, db.First(&v2User).Error)
|
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)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var v3User V3User
|
var v3User V3User
|
||||||
@ -101,7 +99,7 @@ func (ts *TestSuite) testMigrate3To4(t *testing.T) {
|
|||||||
var v3User V3User
|
var v3User V3User
|
||||||
assert.Nil(t, db.First(&v3User).Error)
|
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)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var v4User V4User
|
var v4User V4User
|
||||||
@ -133,7 +131,7 @@ func (ts *TestSuite) testMigrate3To4Collision(t *testing.T) {
|
|||||||
assert.Nil(t, db.First(&v3qux, "username = ?", "qux").Error)
|
assert.Nil(t, db.First(&v3qux, "username = ?", "qux").Error)
|
||||||
assert.Equal(t, "foo", v3qux.PlayerName)
|
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)
|
assert.Nil(t, err)
|
||||||
|
|
||||||
var v4foo V4User
|
var v4foo V4User
|
||||||
@ -146,3 +144,16 @@ func (ts *TestSuite) testMigrate3To4Collision(t *testing.T) {
|
|||||||
assert.Equal(t, 1, len(v4qux.Players))
|
assert.Equal(t, 1, len(v4qux.Players))
|
||||||
assert.Equal(t, "qux", v4qux.Players[0].Name)
|
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"`.
|
- `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: `[]`.
|
- `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`.
|
- `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`.
|
- `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`.
|
- `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/).
|
- `[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"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"github.com/jxskiss/base62"
|
"github.com/jxskiss/base62"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
@ -141,3 +143,23 @@ func (m *KeyedMutex) Lock(key string) func() {
|
|||||||
|
|
||||||
return func() { mtx.Unlock() }
|
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