From 224ce5fe818d1756e4646f585b92153856a626f4 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 26 Nov 2020 22:08:33 +0100 Subject: [PATCH] add revertprovider subcommand Fixes #233 --- README.md | 21 ++++++ cmd/revertprovider.go | 53 +++++++++++++++ dataprovider/bolt.go | 82 ++++++++++++++++++++++ dataprovider/compat.go | 128 +++++++++++++++++++++++++++++++++++ dataprovider/dataprovider.go | 28 ++++++++ dataprovider/memory.go | 4 ++ dataprovider/mysql.go | 43 +++++++++++- dataprovider/pgsql.go | 43 +++++++++++- dataprovider/sqlcommon.go | 79 +++++++++++++++++++++ dataprovider/sqlite.go | 56 ++++++++++++++- 10 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 cmd/revertprovider.go diff --git a/README.md b/README.md index 6d8230c2..405a1a43 100644 --- a/README.md +++ b/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: diff --git a/cmd/revertprovider.go b/cmd/revertprovider.go new file mode 100644 index 00000000..7a22061b --- /dev/null +++ b/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) +} diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go index debda928..e9fec864 100644 --- a/dataprovider/bolt.go +++ b/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") diff --git a/dataprovider/compat.go b/dataprovider/compat.go index 1a31af50..7d6abd88 100644 --- a/dataprovider/compat.go +++ b/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, diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index c2563bb5..256cf65c 100644 --- a/dataprovider/dataprovider.go +++ b/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) { diff --git a/dataprovider/memory.go b/dataprovider/memory.go index 09c9bd08..a54681a0 100644 --- a/dataprovider/memory.go +++ b/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") +} diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go index ac759128..0a919f32 100644 --- a/dataprovider/mysql.go +++ b/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) +} diff --git a/dataprovider/pgsql.go b/dataprovider/pgsql.go index c06f1b34..6b39110c 100644 --- a/dataprovider/pgsql.go +++ b/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) +} diff --git a/dataprovider/sqlcommon.go b/dataprovider/sqlcommon.go index 38284fc1..5c4d1909 100644 --- a/dataprovider/sqlcommon.go +++ b/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) +} diff --git a/dataprovider/sqlite.go b/dataprovider/sqlite.go index 1d698386..83eec78d 100644 --- a/dataprovider/sqlite.go +++ b/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) +}