Ver Fonte

add revertprovider subcommand

Fixes #233
Nicola Murino há 5 anos atrás
pai
commit
224ce5fe81

+ 21 - 0
README.md

@@ -119,6 +119,27 @@ sftpgo initprovider --help
 
 You can disable automatic data provider checks/updates at startup by setting the `update_mode` configuration key to `1`.
 
+If for some reason you want to downgrade SFTPGo, you may need to downgrade your data provider schema and data as well. You can use the `revertprovider` command for this task.
+
+We support the follwing schema versions:
+
+- `6`, this is the current git master
+- `4`, this is the schema for v1.0.0-v1.2.x
+
+So, if you plan to downgrade from git master to 1.2.x, you can prepare your data provider executing the following command from the configuration directory:
+
+```shell
+sftpgo revertprovider --to-version 4
+```
+
+Take a look at the CLI usage to learn how to specify a different configuration file:
+
+```bash
+sftpgo revertprovider --help
+```
+
+The `revertprovider` command is not supported for the memory provider.
+
 ## Users and folders management
 
 After starting SFTPGo you can manage users and folders using:

+ 53 - 0
cmd/revertprovider.go

@@ -0,0 +1,53 @@
+package cmd
+
+import (
+	"os"
+
+	"github.com/rs/zerolog"
+	"github.com/spf13/cobra"
+	"github.com/spf13/viper"
+
+	"github.com/drakkan/sftpgo/config"
+	"github.com/drakkan/sftpgo/dataprovider"
+	"github.com/drakkan/sftpgo/logger"
+	"github.com/drakkan/sftpgo/utils"
+)
+
+var (
+	revertProviderTargetVersion int
+	revertProviderCmd           = &cobra.Command{
+		Use:   "revertprovider",
+		Short: "Revert the configured data provider to a previous version",
+		Long: `This command reads the data provider connection details from the specified
+configuration file and restore the provider schema and/or data to a previous version.
+This command is not supported for the memory provider.
+
+Please take a look at the usage below to customize the options.`,
+		Run: func(cmd *cobra.Command, args []string) {
+			logger.DisableLogger()
+			logger.EnableConsoleLogger(zerolog.DebugLevel)
+			configDir = utils.CleanDirInput(configDir)
+			err := config.LoadConfig(configDir, configFile)
+			if err != nil {
+				logger.WarnToConsole("Unable to initialize data provider, config load error: %v", err)
+				return
+			}
+			providerConf := config.GetProviderConf()
+			logger.InfoToConsole("Reverting provider: %#v config file: %#v target version %v", providerConf.Driver,
+				viper.ConfigFileUsed(), revertProviderTargetVersion)
+			err = dataprovider.RevertDatabase(providerConf, configDir, revertProviderTargetVersion)
+			if err != nil {
+				logger.WarnToConsole("Error reverting provider: %v", err)
+				os.Exit(1)
+			}
+			logger.InfoToConsole("Data provider successfully reverted")
+		},
+	}
+)
+
+func init() {
+	addConfigFlags(revertProviderCmd)
+	revertProviderCmd.Flags().IntVar(&revertProviderTargetVersion, "to-version", 0, `4 means the version supported in v1.0.0-v1.2.x`)
+
+	rootCmd.AddCommand(revertProviderCmd)
+}

+ 82 - 0
dataprovider/bolt.go

@@ -706,6 +706,29 @@ func (p BoltProvider) migrateDatabase() error {
 		return updateBoltDatabaseFromV3(p.dbHandle)
 	case 4:
 		return updateBoltDatabaseFromV4(p.dbHandle)
+	default:
+		if dbVersion.Version > sqlDatabaseVersion {
+			providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
+				boltDatabaseVersion)
+			logger.WarnToConsole("database version %v is newer than the supported: %v", dbVersion.Version,
+				boltDatabaseVersion)
+			return nil
+		}
+		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
+	}
+}
+
+func (p BoltProvider) revertDatabase(targetVersion int) error {
+	dbVersion, err := getBoltDatabaseVersion(p.dbHandle)
+	if err != nil {
+		return err
+	}
+	if dbVersion.Version == targetVersion {
+		return fmt.Errorf("current version match target version, nothing to do")
+	}
+	switch dbVersion.Version {
+	case 5:
+		return downgradeBoltDatabaseFrom5To4(p.dbHandle)
 	default:
 		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
 	}
@@ -846,6 +869,23 @@ func removeUserFromFolderMapping(folder vfs.VirtualFolder, user User, bucket *bo
 	return err
 }
 
+func updateV4BoltCompatUser(dbHandle *bolt.DB, user compatUserV4) error {
+	return dbHandle.Update(func(tx *bolt.Tx) error {
+		bucket, _, err := getBuckets(tx)
+		if err != nil {
+			return err
+		}
+		if u := bucket.Get([]byte(user.Username)); u == nil {
+			return &RecordNotFoundError{err: fmt.Sprintf("username %v does not exist", user.Username)}
+		}
+		buf, err := json.Marshal(user)
+		if err != nil {
+			return err
+		}
+		return bucket.Put([]byte(user.Username), buf)
+	})
+}
+
 func updateV4BoltUser(dbHandle *bolt.DB, user User) error {
 	err := validateUser(&user)
 	if err != nil {
@@ -1027,6 +1067,48 @@ func updateDatabaseFrom3To4(dbHandle *bolt.DB) error {
 	return err
 }
 
+//nolint:dupl
+func downgradeBoltDatabaseFrom5To4(dbHandle *bolt.DB) error {
+	logger.InfoToConsole("downgrading bolt database version: 5 -> 4")
+	providerLog(logger.LevelInfo, "downgrading bolt database version: 5 -> 4")
+	users := []compatUserV4{}
+	err := dbHandle.View(func(tx *bolt.Tx) error {
+		bucket, _, err := getBuckets(tx)
+		if err != nil {
+			return err
+		}
+		cursor := bucket.Cursor()
+		for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
+			var user User
+			err = json.Unmarshal(v, &user)
+			if err != nil {
+				logger.WarnToConsole("failed to unmarshal user %#v to v4, is it already migrated?", string(k))
+				continue
+			}
+			fsConfig, err := convertFsConfigToV4(user.FsConfig, user.Username)
+			if err != nil {
+				return err
+			}
+			users = append(users, convertUserToV4(user, fsConfig))
+		}
+		return nil
+	})
+	if err != nil {
+		return err
+	}
+
+	for _, user := range users {
+		err = updateV4BoltCompatUser(dbHandle, user)
+		if err != nil {
+			return err
+		}
+		providerLog(logger.LevelInfo, "filesystem config updated for user %#v", user.Username)
+	}
+
+	return updateBoltDatabaseVersion(dbHandle, 4)
+}
+
+//nolint:dupl
 func updateDatabaseFrom4To5(dbHandle *bolt.DB) error {
 	logger.InfoToConsole("updating bolt database version: 4 -> 5")
 	providerLog(logger.LevelInfo, "updating bolt database version: 4 -> 5")

+ 128 - 0
dataprovider/compat.go

@@ -1,11 +1,13 @@
 package dataprovider
 
 import (
+	"encoding/json"
 	"fmt"
 	"io/ioutil"
 	"path/filepath"
 
 	"github.com/drakkan/sftpgo/logger"
+	"github.com/drakkan/sftpgo/utils"
 	"github.com/drakkan/sftpgo/vfs"
 )
 
@@ -130,6 +132,34 @@ func createUserFromV4(u compatUserV4, fsConfig Filesystem) User {
 	return user
 }
 
+func convertUserToV4(u User, fsConfig compatFilesystemV4) compatUserV4 {
+	user := compatUserV4{
+		ID:                u.ID,
+		Status:            u.Status,
+		Username:          u.Username,
+		ExpirationDate:    u.ExpirationDate,
+		Password:          u.Password,
+		PublicKeys:        u.PublicKeys,
+		HomeDir:           u.HomeDir,
+		VirtualFolders:    u.VirtualFolders,
+		UID:               u.UID,
+		GID:               u.GID,
+		MaxSessions:       u.MaxSessions,
+		QuotaSize:         u.QuotaSize,
+		QuotaFiles:        u.QuotaFiles,
+		Permissions:       u.Permissions,
+		UsedQuotaSize:     u.UsedQuotaSize,
+		UsedQuotaFiles:    u.UsedQuotaFiles,
+		LastQuotaUpdate:   u.LastQuotaUpdate,
+		UploadBandwidth:   u.UploadBandwidth,
+		DownloadBandwidth: u.DownloadBandwidth,
+		LastLogin:         u.LastLogin,
+		Filters:           u.Filters,
+	}
+	user.FsConfig = fsConfig
+	return user
+}
+
 func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (vfs.Secret, error) {
 	var secret vfs.Secret
 	var err error
@@ -150,6 +180,104 @@ func getCGSCredentialsFromV4(config compatGCSFsConfigV4) (vfs.Secret, error) {
 	return secret, err
 }
 
+func getCGSCredentialsFromV6(config vfs.GCSFsConfig, username string) (string, error) {
+	if config.Credentials.IsEmpty() {
+		config.CredentialFile = filepath.Join(credentialsDirPath, fmt.Sprintf("%v_gcs_credentials.json",
+			username))
+		creds, err := ioutil.ReadFile(config.CredentialFile)
+		if err != nil {
+			return "", err
+		}
+		err = json.Unmarshal(creds, &config.Credentials)
+		if err != nil {
+			return "", err
+		}
+	}
+	if config.Credentials.IsEncrypted() {
+		err := config.Credentials.Decrypt()
+		if err != nil {
+			return "", err
+		}
+		// in V4 GCS credentials were not encrypted
+		return config.Credentials.Payload, nil
+	}
+	return "", nil
+}
+
+func convertFsConfigToV4(fs Filesystem, username string) (compatFilesystemV4, error) {
+	fsV4 := compatFilesystemV4{
+		Provider:     fs.Provider,
+		S3Config:     compatS3FsConfigV4{},
+		AzBlobConfig: compatAzBlobFsConfigV4{},
+		GCSConfig:    compatGCSFsConfigV4{},
+	}
+	switch fs.Provider {
+	case S3FilesystemProvider:
+		fsV4.S3Config = compatS3FsConfigV4{
+			Bucket:            fs.S3Config.Bucket,
+			KeyPrefix:         fs.S3Config.KeyPrefix,
+			Region:            fs.S3Config.Region,
+			AccessKey:         fs.S3Config.AccessKey,
+			AccessSecret:      "",
+			Endpoint:          fs.S3Config.Endpoint,
+			StorageClass:      fs.S3Config.StorageClass,
+			UploadPartSize:    fs.S3Config.UploadPartSize,
+			UploadConcurrency: fs.S3Config.UploadConcurrency,
+		}
+		if fs.S3Config.AccessSecret.IsEncrypted() {
+			err := fs.S3Config.AccessSecret.Decrypt()
+			if err != nil {
+				return fsV4, err
+			}
+			secretV4, err := utils.EncryptData(fs.S3Config.AccessSecret.Payload)
+			if err != nil {
+				return fsV4, err
+			}
+			fsV4.S3Config.AccessSecret = secretV4
+		}
+	case AzureBlobFilesystemProvider:
+		fsV4.AzBlobConfig = compatAzBlobFsConfigV4{
+			Container:         fs.AzBlobConfig.Container,
+			AccountName:       fs.AzBlobConfig.AccountName,
+			AccountKey:        "",
+			Endpoint:          fs.AzBlobConfig.Endpoint,
+			SASURL:            fs.AzBlobConfig.SASURL,
+			KeyPrefix:         fs.AzBlobConfig.KeyPrefix,
+			UploadPartSize:    fs.AzBlobConfig.UploadPartSize,
+			UploadConcurrency: fs.AzBlobConfig.UploadConcurrency,
+			UseEmulator:       fs.AzBlobConfig.UseEmulator,
+			AccessTier:        fs.AzBlobConfig.AccessTier,
+		}
+		if fs.AzBlobConfig.AccountKey.IsEncrypted() {
+			err := fs.AzBlobConfig.AccountKey.Decrypt()
+			if err != nil {
+				return fsV4, err
+			}
+			secretV4, err := utils.EncryptData(fs.AzBlobConfig.AccountKey.Payload)
+			if err != nil {
+				return fsV4, err
+			}
+			fsV4.AzBlobConfig.AccountKey = secretV4
+		}
+	case GCSFilesystemProvider:
+		fsV4.GCSConfig = compatGCSFsConfigV4{
+			Bucket:               fs.GCSConfig.Bucket,
+			KeyPrefix:            fs.GCSConfig.KeyPrefix,
+			CredentialFile:       fs.GCSConfig.CredentialFile,
+			AutomaticCredentials: fs.GCSConfig.AutomaticCredentials,
+			StorageClass:         fs.GCSConfig.StorageClass,
+		}
+		if fs.GCSConfig.AutomaticCredentials == 0 {
+			creds, err := getCGSCredentialsFromV6(fs.GCSConfig, username)
+			if err != nil {
+				return fsV4, err
+			}
+			fsV4.GCSConfig.Credentials = []byte(creds)
+		}
+	}
+	return fsV4, nil
+}
+
 func convertFsConfigFromV4(compatFs compatFilesystemV4, username string) (Filesystem, error) {
 	fsConfig := Filesystem{
 		Provider:     compatFs.Provider,

+ 28 - 0
dataprovider/dataprovider.go

@@ -377,6 +377,7 @@ type Provider interface {
 	reloadConfig() error
 	initializeDatabase() error
 	migrateDatabase() error
+	revertDatabase(targetVersion int) error
 }
 
 // Initialize the data provider.
@@ -477,6 +478,12 @@ func validateSQLTablesPrefix() error {
 func InitializeDatabase(cnf Config, basePath string) error {
 	config = cnf
 
+	if filepath.IsAbs(config.CredentialsPath) {
+		credentialsDirPath = config.CredentialsPath
+	} else {
+		credentialsDirPath = filepath.Join(basePath, config.CredentialsPath)
+	}
+
 	err := createProvider(basePath)
 	if err != nil {
 		return err
@@ -488,6 +495,27 @@ func InitializeDatabase(cnf Config, basePath string) error {
 	return provider.migrateDatabase()
 }
 
+// RevertDatabase restores schema and/or data to a previous version
+func RevertDatabase(cnf Config, basePath string, targetVersion int) error {
+	config = cnf
+
+	if filepath.IsAbs(config.CredentialsPath) {
+		credentialsDirPath = config.CredentialsPath
+	} else {
+		credentialsDirPath = filepath.Join(basePath, config.CredentialsPath)
+	}
+
+	err := createProvider(basePath)
+	if err != nil {
+		return err
+	}
+	err = provider.initializeDatabase()
+	if err != nil && err != ErrNoInitRequired {
+		return err
+	}
+	return provider.revertDatabase(targetVersion)
+}
+
 // CheckUserAndPass retrieves the SFTP user with the given username and password if a match is found or an error
 func CheckUserAndPass(username, password, ip, protocol string) (User, error) {
 	if len(config.ExternalAuthHook) > 0 && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&1 != 0) {

+ 4 - 0
dataprovider/memory.go

@@ -677,3 +677,7 @@ func (p MemoryProvider) initializeDatabase() error {
 func (p MemoryProvider) migrateDatabase() error {
 	return ErrNoInitRequired
 }
+
+func (p MemoryProvider) revertDatabase(targetVersion int) error {
+	return errors.New("memory provider does not store data, revert not possible")
+}

+ 42 - 1
dataprovider/mysql.go

@@ -38,7 +38,8 @@ const (
 		"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `unique_mapping` UNIQUE (`user_id`, `folder_id`);" +
 		"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_folder_id_fk_folders_id` FOREIGN KEY (`folder_id`) REFERENCES `{{folders}}` (`id`) ON DELETE CASCADE;" +
 		"ALTER TABLE `{{folders_mapping}}` ADD CONSTRAINT `folders_mapping_user_id_fk_users_id` FOREIGN KEY (`user_id`) REFERENCES `{{users}}` (`id`) ON DELETE CASCADE;"
-	mysqlV6SQL = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;"
+	mysqlV6SQL     = "ALTER TABLE `{{users}}` ADD COLUMN `additional_info` longtext NULL;"
+	mysqlV6DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `additional_info`;"
 )
 
 // MySQLProvider auth provider for MySQL/MariaDB database
@@ -220,6 +221,35 @@ func (p MySQLProvider) migrateDatabase() error {
 		return updateMySQLDatabaseFromV4(p.dbHandle)
 	case 5:
 		return updateMySQLDatabaseFromV5(p.dbHandle)
+	default:
+		if dbVersion.Version > sqlDatabaseVersion {
+			providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
+				sqlDatabaseVersion)
+			logger.WarnToConsole("database version %v is newer than the supported: %v", dbVersion.Version,
+				sqlDatabaseVersion)
+			return nil
+		}
+		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
+	}
+}
+
+func (p MySQLProvider) revertDatabase(targetVersion int) error {
+	dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
+	if err != nil {
+		return err
+	}
+	if dbVersion.Version == targetVersion {
+		return fmt.Errorf("current version match target version, nothing to do")
+	}
+	switch dbVersion.Version {
+	case 6:
+		err = downgradeMySQLDatabaseFrom6To5(p.dbHandle)
+		if err != nil {
+			return err
+		}
+		return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
+	case 5:
+		return downgradeMySQLDatabaseFrom5To4(p.dbHandle)
 	default:
 		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
 	}
@@ -289,3 +319,14 @@ func updateMySQLDatabaseFrom5To6(dbHandle *sql.DB) error {
 	sql := strings.Replace(mysqlV6SQL, "{{users}}", sqlTableUsers, 1)
 	return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
 }
+
+func downgradeMySQLDatabaseFrom6To5(dbHandle *sql.DB) error {
+	logger.InfoToConsole("downgrading database version: 6 -> 5")
+	providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
+	sql := strings.Replace(mysqlV6DownSQL, "{{users}}", sqlTableUsers, 1)
+	return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 5)
+}
+
+func downgradeMySQLDatabaseFrom5To4(dbHandle *sql.DB) error {
+	return sqlCommonDowngradeDatabaseFrom5To4(dbHandle)
+}

+ 42 - 1
dataprovider/pgsql.go

@@ -37,7 +37,8 @@ ALTER TABLE "{{folders_mapping}}" ADD CONSTRAINT "folders_mapping_user_id_fk_use
 CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
 CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
 `
-	pgsqlV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
+	pgsqlV6SQL     = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
+	pgsqlV6DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "additional_info" CASCADE;`
 )
 
 // PGSQLProvider auth provider for PostgreSQL database
@@ -219,6 +220,35 @@ func (p PGSQLProvider) migrateDatabase() error {
 		return updatePGSQLDatabaseFromV4(p.dbHandle)
 	case 5:
 		return updatePGSQLDatabaseFromV5(p.dbHandle)
+	default:
+		if dbVersion.Version > sqlDatabaseVersion {
+			providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
+				sqlDatabaseVersion)
+			logger.WarnToConsole("database version %v is newer than the supported: %v", dbVersion.Version,
+				sqlDatabaseVersion)
+			return nil
+		}
+		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
+	}
+}
+
+func (p PGSQLProvider) revertDatabase(targetVersion int) error {
+	dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
+	if err != nil {
+		return err
+	}
+	if dbVersion.Version == targetVersion {
+		return fmt.Errorf("current version match target version, nothing to do")
+	}
+	switch dbVersion.Version {
+	case 6:
+		err = downgradePGSQLDatabaseFrom6To5(p.dbHandle)
+		if err != nil {
+			return err
+		}
+		return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
+	case 5:
+		return downgradePGSQLDatabaseFrom5To4(p.dbHandle)
 	default:
 		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
 	}
@@ -288,3 +318,14 @@ func updatePGSQLDatabaseFrom5To6(dbHandle *sql.DB) error {
 	sql := strings.Replace(pgsqlV6SQL, "{{users}}", sqlTableUsers, 1)
 	return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
 }
+
+func downgradePGSQLDatabaseFrom6To5(dbHandle *sql.DB) error {
+	logger.InfoToConsole("downgrading database version: 6 -> 5")
+	providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
+	sql := strings.Replace(pgsqlV6DownSQL, "{{users}}", sqlTableUsers, 1)
+	return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 5)
+}
+
+func downgradePGSQLDatabaseFrom5To4(dbHandle *sql.DB) error {
+	return sqlCommonDowngradeDatabaseFrom5To4(dbHandle)
+}

+ 79 - 0
dataprovider/sqlcommon.go

@@ -948,6 +948,7 @@ func sqlCommonUpdateDatabaseFrom3To4(sqlV4 string, dbHandle *sql.DB) error {
 	return err
 }
 
+//nolint:dupl
 func sqlCommonUpdateDatabaseFrom4To5(dbHandle *sql.DB) error {
 	logger.InfoToConsole("updating database version: 4 -> 5")
 	providerLog(logger.LevelInfo, "updating database version: 4 -> 5")
@@ -1005,6 +1006,26 @@ func sqlCommonUpdateDatabaseFrom4To5(dbHandle *sql.DB) error {
 	return sqlCommonUpdateDatabaseVersion(ctxVersion, dbHandle, 5)
 }
 
+func sqlCommonUpdateV4CompatUser(dbHandle *sql.DB, user compatUserV4) error {
+	ctx, cancel := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
+	defer cancel()
+
+	q := updateCompatV4FsConfigQuery()
+	stmt, err := dbHandle.PrepareContext(ctx, q)
+	if err != nil {
+		providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
+		return err
+	}
+	defer stmt.Close()
+
+	fsConfig, err := json.Marshal(user.FsConfig)
+	if err != nil {
+		return err
+	}
+	_, err = stmt.ExecContext(ctx, string(fsConfig), user.ID)
+	return err
+}
+
 func sqlCommonUpdateV4User(dbHandle *sql.DB, user User) error {
 	err := validateFilesystemConfig(&user)
 	if err != nil {
@@ -1032,3 +1053,61 @@ func sqlCommonUpdateV4User(dbHandle *sql.DB, user User) error {
 	_, err = stmt.ExecContext(ctx, string(fsConfig), user.ID)
 	return err
 }
+
+//nolint:dupl
+func sqlCommonDowngradeDatabaseFrom5To4(dbHandle *sql.DB) error {
+	logger.InfoToConsole("downgrading database version: 5 -> 4")
+	providerLog(logger.LevelInfo, "downgrading database version: 5 -> 4")
+	ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
+	defer cancel()
+	q := getCompatV4FsConfigQuery()
+	stmt, err := dbHandle.PrepareContext(ctx, q)
+	if err != nil {
+		providerLog(logger.LevelWarn, "error preparing database query %#v: %v", q, err)
+		return err
+	}
+	defer stmt.Close()
+	rows, err := stmt.QueryContext(ctx)
+	if err != nil {
+		return err
+	}
+	defer rows.Close()
+
+	users := []compatUserV4{}
+	for rows.Next() {
+		var user User
+		var fsConfigString sql.NullString
+		err = rows.Scan(&user.ID, &user.Username, &fsConfigString)
+		if err != nil {
+			return err
+		}
+		if fsConfigString.Valid {
+			err = json.Unmarshal([]byte(fsConfigString.String), &user.FsConfig)
+			if err != nil {
+				logger.WarnToConsole("failed to unmarshal user %#v to v4, is it already migrated?", user.Username)
+				continue
+			}
+			fsConfig, err := convertFsConfigToV4(user.FsConfig, user.Username)
+			if err != nil {
+				return err
+			}
+			users = append(users, convertUserToV4(user, fsConfig))
+		}
+	}
+	if err := rows.Err(); err != nil {
+		return err
+	}
+
+	for _, user := range users {
+		err = sqlCommonUpdateV4CompatUser(dbHandle, user)
+		if err != nil {
+			return err
+		}
+		providerLog(logger.LevelInfo, "filesystem config downgraded for user %#v", user.Username)
+	}
+
+	ctxVersion, cancelVersion := context.WithTimeout(context.Background(), defaultSQLQueryTimeout)
+	defer cancelVersion()
+
+	return sqlCommonUpdateDatabaseVersion(ctxVersion, dbHandle, 4)
+}

+ 55 - 1
dataprovider/sqlite.go

@@ -63,7 +63,21 @@ ALTER TABLE "new__users" RENAME TO "{{users}}";
 CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
 CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
 `
-	sqliteV6SQL = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
+	sqliteV6SQL     = `ALTER TABLE "{{users}}" ADD COLUMN "additional_info" text NULL;`
+	sqliteV6DownSQL = `CREATE TABLE "new__users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE,
+"password" text NULL, "public_keys" text NULL, "home_dir" varchar(512) NOT NULL, "uid" integer NOT NULL, "gid" integer NOT NULL,
+"max_sessions" integer NOT NULL, "quota_size" bigint NOT NULL, "quota_files" integer NOT NULL, "permissions" text NOT NULL,
+"used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL, "upload_bandwidth" integer NOT NULL,
+"download_bandwidth" integer NOT NULL, "expiration_date" bigint NOT NULL, "last_login" bigint NOT NULL, "status" integer NOT NULL,
+"filters" text NULL, "filesystem" text NULL);
+INSERT INTO "new__users" ("id", "username", "password", "public_keys", "home_dir", "uid", "gid", "max_sessions", "quota_size", "quota_files",
+"permissions", "used_quota_size", "used_quota_files", "last_quota_update", "upload_bandwidth", "download_bandwidth", "expiration_date",
+"last_login", "status", "filters", "filesystem") SELECT "id", "username", "password", "public_keys", "home_dir", "uid", "gid", "max_sessions",
+"quota_size", "quota_files", "permissions", "used_quota_size", "used_quota_files", "last_quota_update", "upload_bandwidth", "download_bandwidth",
+"expiration_date", "last_login", "status", "filters", "filesystem" FROM "{{users}}";
+DROP TABLE "{{users}}";
+ALTER TABLE "new__users" RENAME TO "{{users}}";
+`
 )
 
 // SQLiteProvider auth provider for SQLite database
@@ -242,6 +256,35 @@ func (p SQLiteProvider) migrateDatabase() error {
 		return updateSQLiteDatabaseFromV4(p.dbHandle)
 	case 5:
 		return updateSQLiteDatabaseFromV5(p.dbHandle)
+	default:
+		if dbVersion.Version > sqlDatabaseVersion {
+			providerLog(logger.LevelWarn, "database version %v is newer than the supported: %v", dbVersion.Version,
+				sqlDatabaseVersion)
+			logger.WarnToConsole("database version %v is newer than the supported: %v", dbVersion.Version,
+				sqlDatabaseVersion)
+			return nil
+		}
+		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
+	}
+}
+
+func (p SQLiteProvider) revertDatabase(targetVersion int) error {
+	dbVersion, err := sqlCommonGetDatabaseVersion(p.dbHandle, true)
+	if err != nil {
+		return err
+	}
+	if dbVersion.Version == targetVersion {
+		return fmt.Errorf("current version match target version, nothing to do")
+	}
+	switch dbVersion.Version {
+	case 6:
+		err = downgradeSQLiteDatabaseFrom6To5(p.dbHandle)
+		if err != nil {
+			return err
+		}
+		return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
+	case 5:
+		return downgradeSQLiteDatabaseFrom5To4(p.dbHandle)
 	default:
 		return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
 	}
@@ -311,3 +354,14 @@ func updateSQLiteDatabaseFrom5To6(dbHandle *sql.DB) error {
 	sql := strings.Replace(sqliteV6SQL, "{{users}}", sqlTableUsers, 1)
 	return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 6)
 }
+
+func downgradeSQLiteDatabaseFrom6To5(dbHandle *sql.DB) error {
+	logger.InfoToConsole("downgrading database version: 6 -> 5")
+	providerLog(logger.LevelInfo, "downgrading database version: 6 -> 5")
+	sql := strings.ReplaceAll(sqliteV6DownSQL, "{{users}}", sqlTableUsers)
+	return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 5)
+}
+
+func downgradeSQLiteDatabaseFrom5To4(dbHandle *sql.DB) error {
+	return sqlCommonDowngradeDatabaseFrom5To4(dbHandle)
+}