data providers: add filesystem to folder ...
... and some descriptive fields. The filesystem support for virtual folders will be implemented in future commits
This commit is contained in:
parent
3e1b07324d
commit
2146b83343
20 changed files with 408 additions and 77 deletions
|
@ -59,6 +59,7 @@ type Admin struct {
|
|||
Email string `json:"email"`
|
||||
Permissions []string `json:"permissions"`
|
||||
Filters AdminFilters `json:"filters,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
AdditionalInfo string `json:"additional_info,omitempty"`
|
||||
}
|
||||
|
||||
|
@ -216,6 +217,7 @@ func (a *Admin) getACopy() Admin {
|
|||
Permissions: permissions,
|
||||
Filters: filters,
|
||||
AdditionalInfo: a.AdditionalInfo,
|
||||
Description: a.Description,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -862,7 +862,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if targetVersion == 8 {
|
||||
if targetVersion >= 8 {
|
||||
targetVersion = 6
|
||||
}
|
||||
if dbVersion.Version == targetVersion {
|
||||
|
|
|
@ -63,7 +63,7 @@ const (
|
|||
MemoryDataProviderName = "memory"
|
||||
// DumpVersion defines the version for the dump.
|
||||
// For restore/load we support the current version and the previous one
|
||||
DumpVersion = 6
|
||||
DumpVersion = 7
|
||||
|
||||
argonPwdPrefix = "$argon2id$"
|
||||
bcryptPwdPrefix = "$2a$"
|
||||
|
|
|
@ -39,6 +39,14 @@ const (
|
|||
"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;" +
|
||||
"INSERT INTO {{schema_version}} (version) VALUES (8);"
|
||||
mysqlV9SQL = "ALTER TABLE `{{admins}}` ADD COLUMN `description` varchar(512) NULL;" +
|
||||
"ALTER TABLE `{{folders}}` ADD COLUMN `description` varchar(512) NULL;" +
|
||||
"ALTER TABLE `{{folders}}` ADD COLUMN `filesystem` longtext NULL;" +
|
||||
"ALTER TABLE `{{users}}` ADD COLUMN `description` varchar(512) NULL;"
|
||||
mysqlV9DownSQL = "ALTER TABLE `{{users}}` DROP COLUMN `description`;" +
|
||||
"ALTER TABLE `{{folders}}` DROP COLUMN `filesystem`;" +
|
||||
"ALTER TABLE `{{folders}}` DROP COLUMN `description`;" +
|
||||
"ALTER TABLE `{{admins}}` DROP COLUMN `description`;"
|
||||
)
|
||||
|
||||
// MySQLProvider auth provider for MySQL/MariaDB database
|
||||
|
@ -234,6 +242,8 @@ func (p *MySQLProvider) migrateDatabase() error {
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 8:
|
||||
return updateMySQLDatabaseFromV8(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -254,5 +264,37 @@ func (p *MySQLProvider) revertDatabase(targetVersion int) error {
|
|||
if dbVersion.Version == targetVersion {
|
||||
return errors.New("current version match target version, nothing to do")
|
||||
}
|
||||
return errors.New("the current version cannot be reverted")
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 9:
|
||||
return downgradeMySQLDatabaseFromV9(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFromV8(dbHandle *sql.DB) error {
|
||||
return updateMySQLDatabaseFrom8To9(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFromV9(dbHandle *sql.DB) error {
|
||||
return downgradeMySQLDatabaseFrom9To8(dbHandle)
|
||||
}
|
||||
|
||||
func updateMySQLDatabaseFrom8To9(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 8 -> 9")
|
||||
providerLog(logger.LevelInfo, "updating database version: 8 -> 9")
|
||||
sql := strings.ReplaceAll(mysqlV9SQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 9)
|
||||
}
|
||||
|
||||
func downgradeMySQLDatabaseFrom9To8(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 9 -> 8")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 9 -> 8")
|
||||
sql := strings.ReplaceAll(mysqlV9DownSQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, strings.Split(sql, ";"), 8)
|
||||
}
|
||||
|
|
|
@ -43,6 +43,16 @@ FOREIGN KEY ("user_id") REFERENCES "{{users}}" ("id") MATCH SIMPLE ON UPDATE NO
|
|||
CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
||||
INSERT INTO {{schema_version}} (version) VALUES (8);
|
||||
`
|
||||
pgsqlV9SQL = `ALTER TABLE "{{admins}}" ADD COLUMN "description" varchar(512) NULL;
|
||||
ALTER TABLE "{{folders}}" ADD COLUMN "description" varchar(512) NULL;
|
||||
ALTER TABLE "{{folders}}" ADD COLUMN "filesystem" text NULL;
|
||||
ALTER TABLE "{{users}}" ADD COLUMN "description" varchar(512) NULL;
|
||||
`
|
||||
pgsqlV9DownSQL = `ALTER TABLE "{{users}}" DROP COLUMN "description" CASCADE;
|
||||
ALTER TABLE "{{folders}}" DROP COLUMN "filesystem" CASCADE;
|
||||
ALTER TABLE "{{folders}}" DROP COLUMN "description" CASCADE;
|
||||
ALTER TABLE "{{admins}}" DROP COLUMN "description" CASCADE;
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -240,6 +250,8 @@ func (p *PGSQLProvider) migrateDatabase() error {
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 8:
|
||||
return updatePGSQLDatabaseFromV8(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -260,5 +272,37 @@ func (p *PGSQLProvider) revertDatabase(targetVersion int) error {
|
|||
if dbVersion.Version == targetVersion {
|
||||
return errors.New("current version match target version, nothing to do")
|
||||
}
|
||||
return errors.New("the current version cannot be reverted")
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 9:
|
||||
return downgradePGSQLDatabaseFromV9(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFromV8(dbHandle *sql.DB) error {
|
||||
return updatePGSQLDatabaseFrom8To9(dbHandle)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFromV9(dbHandle *sql.DB) error {
|
||||
return downgradePGSQLDatabaseFrom9To8(dbHandle)
|
||||
}
|
||||
|
||||
func updatePGSQLDatabaseFrom8To9(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 8 -> 9")
|
||||
providerLog(logger.LevelInfo, "updating database version: 8 -> 9")
|
||||
sql := strings.ReplaceAll(pgsqlV9SQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 9)
|
||||
}
|
||||
|
||||
func downgradePGSQLDatabaseFrom9To8(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 9 -> 8")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 9 -> 8")
|
||||
sql := strings.ReplaceAll(pgsqlV9DownSQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 8)
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
sqlDatabaseVersion = 8
|
||||
sqlDatabaseVersion = 9
|
||||
defaultSQLQueryTimeout = 10 * time.Second
|
||||
longSQLQueryTimeout = 60 * time.Second
|
||||
)
|
||||
|
@ -83,7 +83,7 @@ func sqlCommonAddAdmin(admin *Admin, dbHandle *sql.DB) error {
|
|||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, admin.Username, admin.Password, admin.Status, admin.Email, string(perms),
|
||||
string(filters), admin.AdditionalInfo)
|
||||
string(filters), admin.AdditionalInfo, admin.Description)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -114,7 +114,7 @@ func sqlCommonUpdateAdmin(admin *Admin, dbHandle *sql.DB) error {
|
|||
}
|
||||
|
||||
_, err = stmt.ExecContext(ctx, admin.Password, admin.Status, admin.Email, string(perms), string(filters),
|
||||
admin.AdditionalInfo, admin.Username)
|
||||
admin.AdditionalInfo, admin.Description, admin.Username)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -342,7 +342,7 @@ func sqlCommonAddUser(user *User, dbHandle *sql.DB) error {
|
|||
}
|
||||
_, err = stmt.ExecContext(ctx, user.Username, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions, user.QuotaSize,
|
||||
user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status, user.ExpirationDate, string(filters),
|
||||
string(fsConfig), user.AdditionalInfo)
|
||||
string(fsConfig), user.AdditionalInfo, user.Description)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -390,7 +390,7 @@ func sqlCommonUpdateUser(user *User, dbHandle *sql.DB) error {
|
|||
}
|
||||
_, err = stmt.ExecContext(ctx, user.Password, string(publicKeys), user.HomeDir, user.UID, user.GID, user.MaxSessions, user.QuotaSize,
|
||||
user.QuotaFiles, string(permissions), user.UploadBandwidth, user.DownloadBandwidth, user.Status, user.ExpirationDate,
|
||||
string(filters), string(fsConfig), user.AdditionalInfo, user.ID)
|
||||
string(filters), string(fsConfig), user.AdditionalInfo, user.Description, user.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -483,10 +483,10 @@ func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier)
|
|||
|
||||
func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
||||
var admin Admin
|
||||
var email, filters, additionalInfo, permissions sql.NullString
|
||||
var email, filters, additionalInfo, permissions, description sql.NullString
|
||||
|
||||
err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,
|
||||
&filters, &additionalInfo)
|
||||
&filters, &additionalInfo, &description)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
|
@ -517,6 +517,9 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
|
|||
if additionalInfo.Valid {
|
||||
admin.AdditionalInfo = additionalInfo.String
|
||||
}
|
||||
if description.Valid {
|
||||
admin.Description = description.String
|
||||
}
|
||||
|
||||
return admin, err
|
||||
}
|
||||
|
@ -528,12 +531,12 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
|
|||
var publicKey sql.NullString
|
||||
var filters sql.NullString
|
||||
var fsConfig sql.NullString
|
||||
var additionalInfo sql.NullString
|
||||
var additionalInfo, description sql.NullString
|
||||
|
||||
err := row.Scan(&user.ID, &user.Username, &password, &publicKey, &user.HomeDir, &user.UID, &user.GID, &user.MaxSessions,
|
||||
&user.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
|
||||
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
|
||||
&additionalInfo)
|
||||
&additionalInfo, &description)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return user, &RecordNotFoundError{err: err.Error()}
|
||||
|
@ -579,6 +582,9 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
|
|||
if additionalInfo.Valid {
|
||||
user.AdditionalInfo = additionalInfo.String
|
||||
}
|
||||
if description.Valid {
|
||||
user.Description = description.String
|
||||
}
|
||||
user.SetEmptySecretsIfNil()
|
||||
return user, err
|
||||
}
|
||||
|
@ -593,15 +599,18 @@ func sqlCommonCheckFolderExists(ctx context.Context, name string, dbHandle sqlQu
|
|||
}
|
||||
defer stmt.Close()
|
||||
row := stmt.QueryRowContext(ctx, name)
|
||||
var mappedPath sql.NullString
|
||||
var mappedPath, description sql.NullString
|
||||
err = row.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, &folder.LastQuotaUpdate,
|
||||
&folder.Name)
|
||||
&folder.Name, &description)
|
||||
if err == sql.ErrNoRows {
|
||||
return folder, &RecordNotFoundError{err: err.Error()}
|
||||
}
|
||||
if mappedPath.Valid {
|
||||
folder.MappedPath = mappedPath.String
|
||||
}
|
||||
if description.Valid {
|
||||
folder.Description = description.String
|
||||
}
|
||||
return folder, err
|
||||
}
|
||||
|
||||
|
@ -654,7 +663,7 @@ func sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) erro
|
|||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, folder.MappedPath, folder.UsedQuotaSize, folder.UsedQuotaFiles,
|
||||
folder.LastQuotaUpdate, folder.Name)
|
||||
folder.LastQuotaUpdate, folder.Name, folder.Description)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -672,7 +681,7 @@ func sqlCommonUpdateFolder(folder *vfs.BaseVirtualFolder, dbHandle *sql.DB) erro
|
|||
return err
|
||||
}
|
||||
defer stmt.Close()
|
||||
_, err = stmt.ExecContext(ctx, folder.MappedPath, folder.Name)
|
||||
_, err = stmt.ExecContext(ctx, folder.MappedPath, folder.Description, folder.Name)
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -708,15 +717,18 @@ func sqlCommonDumpFolders(dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error)
|
|||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var folder vfs.BaseVirtualFolder
|
||||
var mappedPath sql.NullString
|
||||
var mappedPath, description sql.NullString
|
||||
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||
&folder.LastQuotaUpdate, &folder.Name)
|
||||
&folder.LastQuotaUpdate, &folder.Name, &description)
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
if mappedPath.Valid {
|
||||
folder.MappedPath = mappedPath.String
|
||||
}
|
||||
if description.Valid {
|
||||
folder.Description = description.String
|
||||
}
|
||||
folders = append(folders, folder)
|
||||
}
|
||||
err = rows.Err()
|
||||
|
@ -745,15 +757,18 @@ func sqlCommonGetFolders(limit, offset int, order string, dbHandle sqlQuerier) (
|
|||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
var folder vfs.BaseVirtualFolder
|
||||
var mappedPath sql.NullString
|
||||
var mappedPath, description sql.NullString
|
||||
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
|
||||
&folder.LastQuotaUpdate, &folder.Name)
|
||||
&folder.LastQuotaUpdate, &folder.Name, &description)
|
||||
if err != nil {
|
||||
return folders, err
|
||||
}
|
||||
if mappedPath.Valid {
|
||||
folder.MappedPath = mappedPath.String
|
||||
}
|
||||
if description.Valid {
|
||||
folder.Description = description.String
|
||||
}
|
||||
folders = append(folders, folder)
|
||||
}
|
||||
|
||||
|
|
|
@ -41,6 +41,40 @@ CONSTRAINT "unique_mapping" UNIQUE ("user_id", "folder_id"));
|
|||
CREATE INDEX "folders_mapping_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
|
||||
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
|
||||
INSERT INTO {{schema_version}} (version) VALUES (8);
|
||||
`
|
||||
sqliteV9SQL = `ALTER TABLE "{{admins}}" ADD COLUMN "description" varchar(512) NULL;
|
||||
ALTER TABLE "{{folders}}" ADD COLUMN "description" varchar(512) NULL;
|
||||
ALTER TABLE "{{folders}}" ADD COLUMN "filesystem" text NULL;
|
||||
ALTER TABLE "{{users}}" ADD COLUMN "description" varchar(512) NULL;
|
||||
`
|
||||
sqliteV9DownSQL = `CREATE TABLE "new__users" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "status" integer NOT NULL,
|
||||
"expiration_date" bigint NOT NULL, "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, "last_login" bigint NOT NULL, "filters" text NULL, "filesystem" text NULL,
|
||||
"additional_info" text NULL);
|
||||
INSERT INTO "new__users" ("id", "status", "expiration_date", "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", "last_login", "filters", "filesystem", "additional_info")
|
||||
SELECT "id", "status", "expiration_date", "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", "last_login", "filters", "filesystem", "additional_info" FROM "{{users}}";
|
||||
DROP TABLE "{{users}}";
|
||||
ALTER TABLE "new__users" RENAME TO "{{users}}";
|
||||
CREATE TABLE "new__admins" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "username" varchar(255) NOT NULL UNIQUE,
|
||||
"password" varchar(255) NOT NULL, "email" varchar(255) NULL, "status" integer NOT NULL, "permissions" text NOT NULL,
|
||||
"filters" text NULL, "additional_info" text NULL);
|
||||
INSERT INTO "new__admins" ("id", "username", "password", "email", "status", "permissions", "filters", "additional_info")
|
||||
SELECT "id", "username", "password", "email", "status", "permissions", "filters", "additional_info" FROM "{{admins}}";
|
||||
DROP TABLE "{{admins}}";
|
||||
ALTER TABLE "new__admins" RENAME TO "{{admins}}";
|
||||
CREATE TABLE "new__folders" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(255) NOT NULL UNIQUE,
|
||||
"path" varchar(512) NULL, "used_quota_size" bigint NOT NULL, "used_quota_files" integer NOT NULL, "last_quota_update" bigint NOT NULL);
|
||||
INSERT INTO "new__folders" ("id", "name", "path", "used_quota_size", "used_quota_files", "last_quota_update")
|
||||
SELECT "id", "name", "path", "used_quota_size", "used_quota_files", "last_quota_update" FROM "{{folders}}";
|
||||
DROP TABLE "{{folders}}";
|
||||
ALTER TABLE "new__folders" RENAME TO "{{folders}}";
|
||||
`
|
||||
)
|
||||
|
||||
|
@ -229,6 +263,8 @@ func (p *SQLiteProvider) migrateDatabase() error {
|
|||
providerLog(logger.LevelError, "%v", err)
|
||||
logger.ErrorToConsole("%v", err)
|
||||
return err
|
||||
case version == 8:
|
||||
return updateSQLiteDatabaseFromV8(p.dbHandle)
|
||||
default:
|
||||
if version > sqlDatabaseVersion {
|
||||
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version,
|
||||
|
@ -249,5 +285,53 @@ func (p *SQLiteProvider) revertDatabase(targetVersion int) error {
|
|||
if dbVersion.Version == targetVersion {
|
||||
return errors.New("current version match target version, nothing to do")
|
||||
}
|
||||
return errors.New("the current version cannot be reverted")
|
||||
|
||||
switch dbVersion.Version {
|
||||
case 9:
|
||||
return downgradeSQLiteDatabaseFromV9(p.dbHandle)
|
||||
default:
|
||||
return fmt.Errorf("Database version not handled: %v", dbVersion.Version)
|
||||
}
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFromV8(dbHandle *sql.DB) error {
|
||||
return updateSQLiteDatabaseFrom8To9(dbHandle)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFromV9(dbHandle *sql.DB) error {
|
||||
return downgradeSQLiteDatabaseFrom9To8(dbHandle)
|
||||
}
|
||||
|
||||
func updateSQLiteDatabaseFrom8To9(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("updating database version: 8 -> 9")
|
||||
providerLog(logger.LevelInfo, "updating database version: 8 -> 9")
|
||||
sql := strings.ReplaceAll(sqliteV9SQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
return sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 9)
|
||||
}
|
||||
|
||||
func downgradeSQLiteDatabaseFrom9To8(dbHandle *sql.DB) error {
|
||||
logger.InfoToConsole("downgrading database version: 9 -> 8")
|
||||
providerLog(logger.LevelInfo, "downgrading database version: 9 -> 8")
|
||||
if err := setPragmaFK(dbHandle, "OFF"); err != nil {
|
||||
return err
|
||||
}
|
||||
sql := strings.ReplaceAll(sqliteV9DownSQL, "{{users}}", sqlTableUsers)
|
||||
sql = strings.ReplaceAll(sql, "{{admins}}", sqlTableAdmins)
|
||||
sql = strings.ReplaceAll(sql, "{{folders}}", sqlTableFolders)
|
||||
if err := sqlCommonExecSQLAndUpdateDBVersion(dbHandle, []string{sql}, 8); err != nil {
|
||||
return err
|
||||
}
|
||||
return setPragmaFK(dbHandle, "ON")
|
||||
}
|
||||
|
||||
func setPragmaFK(dbHandle *sql.DB, value string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), longSQLQueryTimeout)
|
||||
defer cancel()
|
||||
|
||||
sql := fmt.Sprintf("PRAGMA foreign_keys=%v;", value)
|
||||
|
||||
_, err := dbHandle.ExecContext(ctx, sql)
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -10,9 +10,10 @@ import (
|
|||
|
||||
const (
|
||||
selectUserFields = "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,additional_info"
|
||||
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name"
|
||||
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info"
|
||||
"used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem," +
|
||||
"additional_info,description"
|
||||
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name,description"
|
||||
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info,description"
|
||||
)
|
||||
|
||||
func getSQLPlaceholders() []string {
|
||||
|
@ -41,15 +42,15 @@ func getDumpAdminsQuery() string {
|
|||
}
|
||||
|
||||
func getAddAdminQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info)
|
||||
VALUES (%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
|
||||
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info,description)
|
||||
VALUES (%v,%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7])
|
||||
}
|
||||
|
||||
func getUpdateAdminQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v
|
||||
return fmt.Sprintf(`UPDATE %v SET password=%v,status=%v,email=%v,permissions=%v,filters=%v,additional_info=%v,description=%v
|
||||
WHERE username = %v`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2],
|
||||
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6])
|
||||
sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7])
|
||||
}
|
||||
|
||||
func getDeleteAdminQuery() string {
|
||||
|
@ -94,20 +95,20 @@ func getQuotaQuery() string {
|
|||
func getAddUserQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (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,status,last_login,expiration_date,filters,
|
||||
filesystem,additional_info)
|
||||
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v,%v,0,%v,%v,%v,%v)`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||
filesystem,additional_info,description)
|
||||
VALUES (%v,%v,%v,%v,%v,%v,%v,%v,%v,%v,0,0,0,%v,%v,%v,0,%v,%v,%v,%v,%v)`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1],
|
||||
sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7],
|
||||
sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13],
|
||||
sqlPlaceholders[14], sqlPlaceholders[15], sqlPlaceholders[16])
|
||||
sqlPlaceholders[14], sqlPlaceholders[15], sqlPlaceholders[16], sqlPlaceholders[17])
|
||||
}
|
||||
|
||||
func getUpdateUserQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %v SET password=%v,public_keys=%v,home_dir=%v,uid=%v,gid=%v,max_sessions=%v,quota_size=%v,
|
||||
quota_files=%v,permissions=%v,upload_bandwidth=%v,download_bandwidth=%v,status=%v,expiration_date=%v,filters=%v,filesystem=%v,
|
||||
additional_info=%v WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],
|
||||
additional_info=%v,description=%v WHERE id = %v`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3],
|
||||
sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
|
||||
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14], sqlPlaceholders[15],
|
||||
sqlPlaceholders[16])
|
||||
sqlPlaceholders[16], sqlPlaceholders[17])
|
||||
}
|
||||
|
||||
func getDeleteUserQuery() string {
|
||||
|
@ -119,12 +120,14 @@ func getFolderByNameQuery() string {
|
|||
}
|
||||
|
||||
func getAddFolderQuery() string {
|
||||
return fmt.Sprintf(`INSERT INTO %v (path,used_quota_size,used_quota_files,last_quota_update,name) VALUES (%v,%v,%v,%v,%v)`,
|
||||
sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4])
|
||||
return fmt.Sprintf(`INSERT INTO %v (path,used_quota_size,used_quota_files,last_quota_update,name,description) VALUES (%v,%v,%v,%v,%v,%v)`,
|
||||
sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
|
||||
sqlPlaceholders[5])
|
||||
}
|
||||
|
||||
func getUpdateFolderQuery() string {
|
||||
return fmt.Sprintf(`UPDATE %v SET path = %v WHERE name = %v`, sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1])
|
||||
return fmt.Sprintf(`UPDATE %v SET path=%v,description=%v WHERE name = %v`, sqlTableFolders, sqlPlaceholders[0],
|
||||
sqlPlaceholders[1], sqlPlaceholders[2])
|
||||
}
|
||||
|
||||
func getDeleteFolderQuery() string {
|
||||
|
|
|
@ -219,6 +219,8 @@ type User struct {
|
|||
Filters UserFilters `json:"filters"`
|
||||
// Filesystem configuration details
|
||||
FsConfig Filesystem `json:"filesystem"`
|
||||
// optional description, for example full name
|
||||
Description string `json:"description,omitempty"`
|
||||
// free form text field for external systems
|
||||
AdditionalInfo string `json:"additional_info,omitempty"`
|
||||
}
|
||||
|
@ -940,6 +942,7 @@ func (u *User) getACopy() User {
|
|||
Filters: filters,
|
||||
FsConfig: fsConfig,
|
||||
AdditionalInfo: u.AdditionalInfo,
|
||||
Description: u.Description,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ docker run --name some-sftpgo -p 127.0.0.1:8080:8080 -p 2022:2022 -d "drakkan/sf
|
|||
|
||||
... where `some-sftpgo` is the name you want to assign to your container, and `tag` is the tag specifying the SFTPGo version you want. See the list above for relevant tags.
|
||||
|
||||
Now visit [http://localhost:8080/](http://localhost:8080/) and create a new SFTPGo user. The SFTP service is available on port 2022.
|
||||
Now visit [http://localhost:8080/](http://localhost:8080/), the default credentials are `admin/password`, and create a new SFTPGo user. The SFTP service is available on port 2022.
|
||||
|
||||
If you prefer GitHub Container Registry to Docker Hub replace `drakkan/sftpgo:tag` with `ghcr.io/drakkan/sftpgo:tag`.
|
||||
|
||||
|
|
|
@ -2033,8 +2033,9 @@ func TestStartQuotaScan(t *testing.T) {
|
|||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
Name: "vfolder",
|
||||
MappedPath: filepath.Join(os.TempDir(), "folder"),
|
||||
Name: "vfolder",
|
||||
MappedPath: filepath.Join(os.TempDir(), "folder"),
|
||||
Description: "virtual folder",
|
||||
}
|
||||
_, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -2444,9 +2445,11 @@ func TestFolders(t *testing.T) {
|
|||
_, _, err = httpdtest.UpdateFolder(folder1, http.StatusBadRequest)
|
||||
assert.NoError(t, err)
|
||||
folder1.MappedPath = filepath.Join(os.TempDir(), "updated")
|
||||
folder1.Description = "updated folder description"
|
||||
f, _, err = httpdtest.UpdateFolder(folder1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, folder1.MappedPath, f.MappedPath)
|
||||
assert.Equal(t, folder1.Description, f.Description)
|
||||
|
||||
_, err = httpdtest.RemoveFolder(folder1, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -2632,6 +2635,7 @@ func TestLoaddataFromPostBody(t *testing.T) {
|
|||
func TestLoaddata(t *testing.T) {
|
||||
mappedPath := filepath.Join(os.TempDir(), "restored_folder")
|
||||
folderName := filepath.Base(mappedPath)
|
||||
foldeDesc := "restored folder desc"
|
||||
user := getTestUser()
|
||||
user.ID = 1
|
||||
user.Username = "test_user_restore"
|
||||
|
@ -2651,8 +2655,9 @@ func TestLoaddata(t *testing.T) {
|
|||
Users: []string{"user"},
|
||||
},
|
||||
{
|
||||
MappedPath: mappedPath,
|
||||
Name: folderName,
|
||||
MappedPath: mappedPath,
|
||||
Name: folderName,
|
||||
Description: foldeDesc,
|
||||
},
|
||||
}
|
||||
backupContent, err := json.Marshal(backupData)
|
||||
|
@ -2698,6 +2703,7 @@ func TestLoaddata(t *testing.T) {
|
|||
assert.Equal(t, int64(123), folder.UsedQuotaSize)
|
||||
assert.Equal(t, 456, folder.UsedQuotaFiles)
|
||||
assert.Equal(t, int64(789), folder.LastQuotaUpdate)
|
||||
assert.Equal(t, foldeDesc, folder.Description)
|
||||
assert.Len(t, folder.Users, 0)
|
||||
_, err = httpdtest.RemoveFolder(folder, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -2765,7 +2771,9 @@ func TestLoaddataMode(t *testing.T) {
|
|||
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
oldInfo := admin.AdditionalInfo
|
||||
oldDesc := admin.Description
|
||||
admin.AdditionalInfo = "newInfo"
|
||||
admin.Description = "newDesc"
|
||||
admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
||||
|
@ -2797,6 +2805,7 @@ func TestLoaddataMode(t *testing.T) {
|
|||
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.NotEqual(t, oldInfo, admin.AdditionalInfo)
|
||||
assert.NotEqual(t, oldDesc, admin.Description)
|
||||
|
||||
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "2", http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -4183,6 +4192,7 @@ func TestWebAdminBasicMock(t *testing.T) {
|
|||
form.Set("password", "")
|
||||
form.Set("status", "1")
|
||||
form.Set("permissions", "*")
|
||||
form.Set("description", admin.Description)
|
||||
req, _ := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))
|
||||
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -4525,6 +4535,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
user.DownloadBandwidth = 64
|
||||
user.UID = 1000
|
||||
user.AdditionalInfo = "info"
|
||||
user.Description = "user dsc"
|
||||
mappedDir := filepath.Join(os.TempDir(), "mapped")
|
||||
folderName := filepath.Base(mappedDir)
|
||||
f := vfs.BaseVirtualFolder{
|
||||
|
@ -4553,6 +4564,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
form.Set("allowed_patterns", "/dir2::*.jpg,*.png\n/dir1::*.png")
|
||||
form.Set("denied_patterns", "/dir1::*.zip\n/dir3::*.rar\n/dir2::*.mkv")
|
||||
form.Set("additional_info", user.AdditionalInfo)
|
||||
form.Set("description", user.Description)
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
// test invalid url escape
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b)
|
||||
|
@ -4701,6 +4713,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
assert.Equal(t, user.DownloadBandwidth, newUser.DownloadBandwidth)
|
||||
assert.Equal(t, int64(1000), newUser.Filters.MaxUploadFileSize)
|
||||
assert.Equal(t, user.AdditionalInfo, newUser.AdditionalInfo)
|
||||
assert.Equal(t, user.Description, newUser.Description)
|
||||
assert.True(t, utils.IsStringInSlice(testPubKey, newUser.PublicKeys))
|
||||
if val, ok := newUser.Permissions["/subdir"]; ok {
|
||||
assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val))
|
||||
|
@ -4813,6 +4826,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "100")
|
||||
form.Set("disconnect", "1")
|
||||
form.Set("additional_info", user.AdditionalInfo)
|
||||
form.Set("description", user.Description)
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
|
@ -4873,6 +4887,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
assert.Equal(t, user.UID, updateUser.UID)
|
||||
assert.Equal(t, user.GID, updateUser.GID)
|
||||
assert.Equal(t, user.AdditionalInfo, updateUser.AdditionalInfo)
|
||||
assert.Equal(t, user.Description, updateUser.Description)
|
||||
assert.Equal(t, int64(100), updateUser.Filters.MaxUploadFileSize)
|
||||
|
||||
if val, ok := updateUser.Permissions["/otherdir"]; ok {
|
||||
|
@ -4903,8 +4918,9 @@ func TestRenderFolderTemplateMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
Name: "templatefolder",
|
||||
MappedPath: filepath.Join(os.TempDir(), "mapped"),
|
||||
Name: "templatefolder",
|
||||
MappedPath: filepath.Join(os.TempDir(), "mapped"),
|
||||
Description: "template folder desc",
|
||||
}
|
||||
folder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
|
@ -4977,8 +4993,9 @@ func TestRenderWebCloneUserMock(t *testing.T) {
|
|||
|
||||
func TestUserTemplateWithFoldersMock(t *testing.T) {
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
Name: "vfolder",
|
||||
MappedPath: filepath.Join(os.TempDir(), "mapped"),
|
||||
Name: "vfolder",
|
||||
MappedPath: filepath.Join(os.TempDir(), "mapped"),
|
||||
Description: "vfolder desc with spéciàl ch@rs",
|
||||
}
|
||||
|
||||
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass)
|
||||
|
@ -5002,6 +5019,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
form.Set("expiration_date", "2020-01-01 00:00:00")
|
||||
form.Set("fs_provider", "0")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("description", "desc %username% %password%")
|
||||
form.Set("virtual_folders", "/vdir%username%::"+folder.Name+"::-1::-1")
|
||||
form.Set("users", "auser1::password1\nauser2::password2::"+testPubKey+"\nauser1::password")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
|
@ -5041,10 +5059,13 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
folder1 := dump.Folders[0]
|
||||
assert.Equal(t, "auser1", user1.Username)
|
||||
assert.Equal(t, "auser2", user2.Username)
|
||||
assert.Equal(t, "desc auser1 password1", user1.Description)
|
||||
assert.Equal(t, "desc auser2 password2", user2.Description)
|
||||
assert.Equal(t, filepath.Join(os.TempDir(), user1.Username), user1.HomeDir)
|
||||
assert.Equal(t, filepath.Join(os.TempDir(), user2.Username), user2.HomeDir)
|
||||
assert.Equal(t, folder.Name, folder1.Name)
|
||||
assert.Equal(t, folder.MappedPath, folder1.MappedPath)
|
||||
assert.Equal(t, folder.Description, folder1.Description)
|
||||
assert.Len(t, user1.PublicKeys, 0)
|
||||
assert.Len(t, user2.PublicKeys, 1)
|
||||
assert.Len(t, user1.VirtualFolders, 1)
|
||||
|
@ -5178,6 +5199,7 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
form := make(url.Values)
|
||||
form.Set("name", folderName)
|
||||
form.Set("mapped_path", mappedPath)
|
||||
form.Set("description", "desc folder %name%")
|
||||
form.Set("folders", "folder1\nfolder2\nfolder3\nfolder1\n\n\n")
|
||||
contentType := "application/x-www-form-urlencoded"
|
||||
req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode())))
|
||||
|
@ -5209,10 +5231,13 @@ func TestFolderTemplateMock(t *testing.T) {
|
|||
require.Len(t, dump.Admins, 0)
|
||||
require.Len(t, dump.Folders, 3)
|
||||
require.Equal(t, "folder1", dump.Folders[0].Name)
|
||||
require.Equal(t, "desc folder folder1", dump.Folders[0].Description)
|
||||
require.True(t, strings.HasSuffix(dump.Folders[0].MappedPath, "folder1mappedfolder1path"))
|
||||
require.Equal(t, "folder2", dump.Folders[1].Name)
|
||||
require.Equal(t, "desc folder folder2", dump.Folders[1].Description)
|
||||
require.True(t, strings.HasSuffix(dump.Folders[1].MappedPath, "folder2mappedfolder2path"))
|
||||
require.Equal(t, "folder3", dump.Folders[2].Name)
|
||||
require.Equal(t, "desc folder folder3", dump.Folders[2].Description)
|
||||
require.True(t, strings.HasSuffix(dump.Folders[2].MappedPath, "folder3mappedfolder3path"))
|
||||
|
||||
form.Set("folders", "\n\n\n")
|
||||
|
@ -5258,6 +5283,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/"
|
||||
user.FsConfig.S3Config.UploadPartSize = 5
|
||||
user.FsConfig.S3Config.UploadConcurrency = 4
|
||||
user.Description = "s3 tèst user"
|
||||
form := make(url.Values)
|
||||
form.Set(csrfFormToken, csrfToken)
|
||||
form.Set("username", user.Username)
|
||||
|
@ -5287,6 +5313,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("allowed_extensions", "/dir1::.jpg,.png")
|
||||
form.Set("denied_extensions", "/dir2::.zip")
|
||||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("description", user.Description)
|
||||
// test invalid s3_upload_part_size
|
||||
form.Set("s3_upload_part_size", "a")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
|
@ -5333,6 +5360,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
assert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload())
|
||||
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey())
|
||||
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData())
|
||||
assert.Equal(t, user.Description, updateUser.Description)
|
||||
// now check that a redacted password is not saved
|
||||
form.Set("s3_access_secret", redactedSecret)
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
|
@ -5819,9 +5847,11 @@ func TestAddWebFoldersMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
mappedPath := filepath.Clean(os.TempDir())
|
||||
folderName := filepath.Base(mappedPath)
|
||||
folderDesc := "a simple desc"
|
||||
form := make(url.Values)
|
||||
form.Set("mapped_path", mappedPath)
|
||||
form.Set("name", folderName)
|
||||
form.Set("description", folderDesc)
|
||||
req, err := http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode()))
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
|
@ -5868,6 +5898,7 @@ func TestAddWebFoldersMock(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Equal(t, mappedPath, folder.MappedPath)
|
||||
assert.Equal(t, folderName, folder.Name)
|
||||
assert.Equal(t, folderDesc, folder.Description)
|
||||
// cleanup
|
||||
req, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
|
@ -5883,9 +5914,11 @@ func TestUpdateWebFolderMock(t *testing.T) {
|
|||
csrfToken, err := getCSRFToken()
|
||||
assert.NoError(t, err)
|
||||
folderName := "vfolderupdate"
|
||||
folderDesc := "updated desc"
|
||||
folder := vfs.BaseVirtualFolder{
|
||||
Name: folderName,
|
||||
MappedPath: filepath.Join(os.TempDir(), "folderupdate"),
|
||||
Name: folderName,
|
||||
MappedPath: filepath.Join(os.TempDir(), "folderupdate"),
|
||||
Description: "dsc",
|
||||
}
|
||||
_, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
|
||||
newMappedPath := folder.MappedPath + "1"
|
||||
|
@ -5893,6 +5926,7 @@ func TestUpdateWebFolderMock(t *testing.T) {
|
|||
form := make(url.Values)
|
||||
form.Set("mapped_path", newMappedPath)
|
||||
form.Set("name", folderName)
|
||||
form.Set("description", folderDesc)
|
||||
form.Set(csrfFormToken, "")
|
||||
req, err := http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), strings.NewReader(form.Encode()))
|
||||
assert.NoError(t, err)
|
||||
|
@ -5910,6 +5944,16 @@ func TestUpdateWebFolderMock(t *testing.T) {
|
|||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusSeeOther, rr)
|
||||
|
||||
req, _ = http.NewRequest(http.MethodGet, path.Join(folderPath, folderName), nil)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
err = render.DecodeJSON(rr.Body, &folder)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, newMappedPath, folder.MappedPath)
|
||||
assert.Equal(t, folderName, folder.Name)
|
||||
assert.Equal(t, folderDesc, folder.Description)
|
||||
|
||||
// parse form error
|
||||
req, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName)+"??a=a%B3%A2%G3", strings.NewReader(form.Encode()))
|
||||
assert.NoError(t, err)
|
||||
|
@ -5976,29 +6020,57 @@ func TestWebFoldersMock(t *testing.T) {
|
|||
mappedPath2 := filepath.Join(os.TempDir(), "vfolder2")
|
||||
folderName1 := filepath.Base(mappedPath1)
|
||||
folderName2 := filepath.Base(mappedPath2)
|
||||
folderDesc1 := "vfolder1 desc"
|
||||
folderDesc2 := "vfolder2 desc"
|
||||
folders := []vfs.BaseVirtualFolder{
|
||||
{
|
||||
Name: folderName1,
|
||||
MappedPath: mappedPath1,
|
||||
Name: folderName1,
|
||||
MappedPath: mappedPath1,
|
||||
Description: folderDesc1,
|
||||
},
|
||||
{
|
||||
Name: folderName2,
|
||||
MappedPath: mappedPath2,
|
||||
Name: folderName2,
|
||||
MappedPath: mappedPath2,
|
||||
Description: folderDesc2,
|
||||
},
|
||||
}
|
||||
for _, folder := range folders {
|
||||
folderAsJSON, err := json.Marshal(folder)
|
||||
assert.NoError(t, err)
|
||||
req, _ := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON))
|
||||
req, err := http.NewRequest(http.MethodPost, folderPath, bytes.NewBuffer(folderAsJSON))
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusCreated, rr)
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, webFoldersPath, nil)
|
||||
req, err := http.NewRequest(http.MethodGet, folderPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setBearerForReq(req, apiToken)
|
||||
rr := executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
var foldersGet []vfs.BaseVirtualFolder
|
||||
err = render.DecodeJSON(rr.Body, &foldersGet)
|
||||
assert.NoError(t, err)
|
||||
numFound := 0
|
||||
for _, f := range foldersGet {
|
||||
if f.Name == folderName1 {
|
||||
assert.Equal(t, mappedPath1, f.MappedPath)
|
||||
assert.Equal(t, folderDesc1, f.Description)
|
||||
numFound++
|
||||
}
|
||||
if f.Name == folderName2 {
|
||||
assert.Equal(t, mappedPath2, f.MappedPath)
|
||||
assert.Equal(t, folderDesc2, f.Description)
|
||||
numFound++
|
||||
}
|
||||
}
|
||||
assert.Equal(t, 2, numFound)
|
||||
|
||||
req, err = http.NewRequest(http.MethodGet, webFoldersPath, nil)
|
||||
assert.NoError(t, err)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
rr := executeRequest(req)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
req, err = http.NewRequest(http.MethodGet, webFoldersPath+"?qlimit=a", nil)
|
||||
assert.NoError(t, err)
|
||||
|
@ -6145,15 +6217,17 @@ func getTestAdmin() dataprovider.Admin {
|
|||
Status: 1,
|
||||
Permissions: []string{dataprovider.PermAdminAny},
|
||||
Email: "admin@example.com",
|
||||
Description: "test admin",
|
||||
}
|
||||
}
|
||||
|
||||
func getTestUser() dataprovider.User {
|
||||
user := dataprovider.User{
|
||||
Username: defaultUsername,
|
||||
Password: defaultPassword,
|
||||
HomeDir: filepath.Join(homeBasePath, defaultUsername),
|
||||
Status: 1,
|
||||
Username: defaultUsername,
|
||||
Password: defaultPassword,
|
||||
HomeDir: filepath.Join(homeBasePath, defaultUsername),
|
||||
Status: 1,
|
||||
Description: "test user",
|
||||
}
|
||||
user.Permissions = make(map[string][]string)
|
||||
user.Permissions["/"] = defaultPerms
|
||||
|
|
|
@ -2,7 +2,7 @@ openapi: 3.0.3
|
|||
info:
|
||||
title: SFTPGo
|
||||
description: SFTPGo REST API
|
||||
version: 2.4.4
|
||||
version: 2.4.5
|
||||
|
||||
servers:
|
||||
- url: /api/v2
|
||||
|
@ -1560,6 +1560,9 @@ components:
|
|||
mapped_path:
|
||||
type: string
|
||||
description: absolute filesystem path to use as virtual folder
|
||||
description:
|
||||
type: string
|
||||
description: optional description
|
||||
used_quota_size:
|
||||
type: integer
|
||||
format: int64
|
||||
|
@ -1575,8 +1578,6 @@ components:
|
|||
items:
|
||||
type: string
|
||||
description: list of usernames associated with this virtual folder
|
||||
required:
|
||||
- mapped_path
|
||||
description: defines the path for the virtual folder and the used quota limits. The same folder can be shared among multiple users and each user can have different quota limits or a different virtual path.
|
||||
VirtualFolder:
|
||||
allOf:
|
||||
|
@ -1615,6 +1616,9 @@ components:
|
|||
username:
|
||||
type: string
|
||||
description: username is unique
|
||||
description:
|
||||
type: string
|
||||
description: optional description, for example the user full name
|
||||
expiration_date:
|
||||
type: integer
|
||||
format: int64
|
||||
|
@ -1723,6 +1727,9 @@ components:
|
|||
username:
|
||||
type: string
|
||||
description: username is unique
|
||||
description:
|
||||
type: string
|
||||
description: optional description, for example the admin full name
|
||||
password:
|
||||
type: string
|
||||
format: password
|
||||
|
|
|
@ -802,6 +802,7 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
|
|||
admin.Status = status
|
||||
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
|
||||
admin.AdditionalInfo = r.Form.Get("additional_info")
|
||||
admin.Description = r.Form.Get("description")
|
||||
return admin, nil
|
||||
}
|
||||
|
||||
|
@ -818,6 +819,7 @@ func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVi
|
|||
replacements["%name%"] = folder.Name
|
||||
|
||||
folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)
|
||||
folder.Description = replacePlaceholders(folder.Description, replacements)
|
||||
|
||||
return folder
|
||||
}
|
||||
|
@ -887,6 +889,7 @@ func getUserFromTemplate(user dataprovider.User, template userTemplateFields) da
|
|||
vfolders = append(vfolders, vfolder)
|
||||
}
|
||||
user.VirtualFolders = vfolders
|
||||
user.Description = replacePlaceholders(user.Description, replacements)
|
||||
user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)
|
||||
|
||||
switch user.FsConfig.Provider {
|
||||
|
@ -977,6 +980,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
|
|||
Filters: getFiltersFromUserPostFields(r),
|
||||
FsConfig: fsConfig,
|
||||
AdditionalInfo: r.Form.Get("additional_info"),
|
||||
Description: r.Form.Get("description"),
|
||||
}
|
||||
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
|
||||
user.Filters.MaxUploadFileSize = maxFileSize
|
||||
|
@ -1250,6 +1254,7 @@ func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
templateFolder.MappedPath = r.Form.Get("mapped_path")
|
||||
templateFolder.Description = r.Form.Get("description")
|
||||
|
||||
var dump dataprovider.BackupData
|
||||
dump.Version = dataprovider.DumpVersion
|
||||
|
@ -1458,6 +1463,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
folder.MappedPath = r.Form.Get("mapped_path")
|
||||
folder.Name = r.Form.Get("name")
|
||||
folder.Description = r.Form.Get("description")
|
||||
|
||||
err = dataprovider.AddFolder(&folder)
|
||||
if err == nil {
|
||||
|
@ -1501,6 +1507,7 @@ func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
|
|||
return
|
||||
}
|
||||
folder.MappedPath = r.Form.Get("mapped_path")
|
||||
folder.Description = r.Form.Get("description")
|
||||
err = dataprovider.UpdateFolder(&folder)
|
||||
if err != nil {
|
||||
renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())
|
||||
|
|
|
@ -845,6 +845,9 @@ func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder)
|
|||
if expected.UsedQuotaFiles != actual.UsedQuotaFiles {
|
||||
return errors.New("used quota files mismatch")
|
||||
}
|
||||
if expected.Description != actual.Description {
|
||||
return errors.New("Description mismatch")
|
||||
}
|
||||
if len(expected.Users) != len(actual.Users) {
|
||||
return errors.New("folder users mismatch")
|
||||
}
|
||||
|
@ -869,17 +872,8 @@ func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error
|
|||
return errors.New("admin ID mismatch")
|
||||
}
|
||||
}
|
||||
if expected.Username != actual.Username {
|
||||
return errors.New("Username mismatch")
|
||||
}
|
||||
if expected.Email != actual.Email {
|
||||
return errors.New("Email mismatch")
|
||||
}
|
||||
if expected.Status != actual.Status {
|
||||
return errors.New("Status mismatch")
|
||||
}
|
||||
if expected.AdditionalInfo != actual.AdditionalInfo {
|
||||
return errors.New("AdditionalInfo mismatch")
|
||||
if err := compareAdminEqualFields(expected, actual); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(expected.Permissions) != len(actual.Permissions) {
|
||||
return errors.New("Permissions mismatch")
|
||||
|
@ -901,6 +895,25 @@ func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func compareAdminEqualFields(expected *dataprovider.Admin, actual *dataprovider.Admin) error {
|
||||
if expected.Username != actual.Username {
|
||||
return errors.New("Username mismatch")
|
||||
}
|
||||
if expected.Email != actual.Email {
|
||||
return errors.New("Email mismatch")
|
||||
}
|
||||
if expected.Status != actual.Status {
|
||||
return errors.New("Status mismatch")
|
||||
}
|
||||
if expected.Description != actual.Description {
|
||||
return errors.New("Description mismatch")
|
||||
}
|
||||
if expected.AdditionalInfo != actual.AdditionalInfo {
|
||||
return errors.New("AdditionalInfo mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkUser(expected *dataprovider.User, actual *dataprovider.User) error {
|
||||
if actual.Password != "" {
|
||||
return errors.New("User password must not be visible")
|
||||
|
@ -1274,6 +1287,9 @@ func compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.U
|
|||
if expected.AdditionalInfo != actual.AdditionalInfo {
|
||||
return errors.New("AdditionalInfo mismatch")
|
||||
}
|
||||
if expected.Description != actual.Description {
|
||||
return errors.New("Description mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -257,7 +257,7 @@ func (s *Service) loadInitialData() error {
|
|||
if err != nil {
|
||||
return fmt.Errorf("unable to parse file to restore %#v: %v", s.LoadDataFrom, err)
|
||||
}
|
||||
err = s.restoreDump(dump)
|
||||
err = s.restoreDump(&dump)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -278,7 +278,7 @@ func (s *Service) loadInitialData() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (s *Service) restoreDump(dump dataprovider.BackupData) error {
|
||||
func (s *Service) restoreDump(dump *dataprovider.BackupData) error {
|
||||
err := httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)
|
||||
|
|
|
@ -22,6 +22,17 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
|
||||
value="{{.Admin.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
|
||||
<small id="descriptionHelpBlock" class="form-text text-muted">
|
||||
Optional description, for example the admin full name
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -49,6 +49,16 @@
|
|||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-group row">
|
||||
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
|
||||
value="{{.Folder.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
|
||||
<small id="descriptionHelpBlock" class="form-text text-muted">
|
||||
Optional description
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -61,6 +61,17 @@
|
|||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="text" class="form-control" id="idDescription" name="description" placeholder=""
|
||||
value="{{.User.Description}}" maxlength="255" aria-describedby="descriptionHelpBlock">
|
||||
<small id="descriptionHelpBlock" class="form-text text-muted">
|
||||
Optional description, for example the user full name
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idStatus" class="col-sm-2 col-form-label">Status</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -2,7 +2,7 @@ package version
|
|||
|
||||
import "strings"
|
||||
|
||||
const version = "2.0.90-dev"
|
||||
const version = "2.0.2-dev"
|
||||
|
||||
var (
|
||||
commit = ""
|
||||
|
|
|
@ -15,6 +15,7 @@ type BaseVirtualFolder struct {
|
|||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MappedPath string `json:"mapped_path,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
UsedQuotaSize int64 `json:"used_quota_size"`
|
||||
// Used quota as number of files
|
||||
UsedQuotaFiles int `json:"used_quota_files"`
|
||||
|
@ -31,6 +32,7 @@ func (v *BaseVirtualFolder) GetACopy() BaseVirtualFolder {
|
|||
return BaseVirtualFolder{
|
||||
ID: v.ID,
|
||||
Name: v.Name,
|
||||
Description: v.Description,
|
||||
MappedPath: v.MappedPath,
|
||||
UsedQuotaSize: v.UsedQuotaSize,
|
||||
UsedQuotaFiles: v.UsedQuotaFiles,
|
||||
|
|
Loading…
Reference in a new issue