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:
Nicola Murino 2021-02-24 19:40:29 +01:00
parent 3e1b07324d
commit 2146b83343
No known key found for this signature in database
GPG key ID: 2F1FB59433D5A8CB
20 changed files with 408 additions and 77 deletions

View file

@ -59,6 +59,7 @@ type Admin struct {
Email string `json:"email"` Email string `json:"email"`
Permissions []string `json:"permissions"` Permissions []string `json:"permissions"`
Filters AdminFilters `json:"filters,omitempty"` Filters AdminFilters `json:"filters,omitempty"`
Description string `json:"description,omitempty"`
AdditionalInfo string `json:"additional_info,omitempty"` AdditionalInfo string `json:"additional_info,omitempty"`
} }
@ -216,6 +217,7 @@ func (a *Admin) getACopy() Admin {
Permissions: permissions, Permissions: permissions,
Filters: filters, Filters: filters,
AdditionalInfo: a.AdditionalInfo, AdditionalInfo: a.AdditionalInfo,
Description: a.Description,
} }
} }

View file

@ -862,7 +862,7 @@ func (p *BoltProvider) revertDatabase(targetVersion int) error {
if err != nil { if err != nil {
return err return err
} }
if targetVersion == 8 { if targetVersion >= 8 {
targetVersion = 6 targetVersion = 6
} }
if dbVersion.Version == targetVersion { if dbVersion.Version == targetVersion {

View file

@ -63,7 +63,7 @@ const (
MemoryDataProviderName = "memory" MemoryDataProviderName = "memory"
// DumpVersion defines the version for the dump. // DumpVersion defines the version for the dump.
// For restore/load we support the current version and the previous one // For restore/load we support the current version and the previous one
DumpVersion = 6 DumpVersion = 7
argonPwdPrefix = "$argon2id$" argonPwdPrefix = "$argon2id$"
bcryptPwdPrefix = "$2a$" bcryptPwdPrefix = "$2a$"

View file

@ -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_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;" + "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);" "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 // MySQLProvider auth provider for MySQL/MariaDB database
@ -234,6 +242,8 @@ func (p *MySQLProvider) migrateDatabase() error {
providerLog(logger.LevelError, "%v", err) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 8:
return updateMySQLDatabaseFromV8(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version, 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 { if dbVersion.Version == targetVersion {
return errors.New("current version match target version, nothing to do") 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)
} }

View file

@ -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_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id"); CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
INSERT INTO {{schema_version}} (version) VALUES (8); 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) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 8:
return updatePGSQLDatabaseFromV8(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version, 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 { if dbVersion.Version == targetVersion {
return errors.New("current version match target version, nothing to do") 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)
} }

View file

@ -15,7 +15,7 @@ import (
) )
const ( const (
sqlDatabaseVersion = 8 sqlDatabaseVersion = 9
defaultSQLQueryTimeout = 10 * time.Second defaultSQLQueryTimeout = 10 * time.Second
longSQLQueryTimeout = 60 * 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), _, 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 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), _, 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 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, _, 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), 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 { if err != nil {
return err 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, _, 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, 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 { if err != nil {
return err return err
} }
@ -483,10 +483,10 @@ func sqlCommonGetUsers(limit int, offset int, order string, dbHandle sqlQuerier)
func getAdminFromDbRow(row sqlScanner) (Admin, error) { func getAdminFromDbRow(row sqlScanner) (Admin, error) {
var admin Admin 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, err := row.Scan(&admin.ID, &admin.Username, &admin.Password, &admin.Status, &email, &permissions,
&filters, &additionalInfo) &filters, &additionalInfo, &description)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
@ -517,6 +517,9 @@ func getAdminFromDbRow(row sqlScanner) (Admin, error) {
if additionalInfo.Valid { if additionalInfo.Valid {
admin.AdditionalInfo = additionalInfo.String admin.AdditionalInfo = additionalInfo.String
} }
if description.Valid {
admin.Description = description.String
}
return admin, err return admin, err
} }
@ -528,12 +531,12 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
var publicKey sql.NullString var publicKey sql.NullString
var filters sql.NullString var filters sql.NullString
var fsConfig 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, 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.QuotaSize, &user.QuotaFiles, &permissions, &user.UsedQuotaSize, &user.UsedQuotaFiles, &user.LastQuotaUpdate,
&user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig, &user.UploadBandwidth, &user.DownloadBandwidth, &user.ExpirationDate, &user.LastLogin, &user.Status, &filters, &fsConfig,
&additionalInfo) &additionalInfo, &description)
if err != nil { if err != nil {
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return user, &RecordNotFoundError{err: err.Error()} return user, &RecordNotFoundError{err: err.Error()}
@ -579,6 +582,9 @@ func getUserFromDbRow(row sqlScanner) (User, error) {
if additionalInfo.Valid { if additionalInfo.Valid {
user.AdditionalInfo = additionalInfo.String user.AdditionalInfo = additionalInfo.String
} }
if description.Valid {
user.Description = description.String
}
user.SetEmptySecretsIfNil() user.SetEmptySecretsIfNil()
return user, err return user, err
} }
@ -593,15 +599,18 @@ func sqlCommonCheckFolderExists(ctx context.Context, name string, dbHandle sqlQu
} }
defer stmt.Close() defer stmt.Close()
row := stmt.QueryRowContext(ctx, name) 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, err = row.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, &folder.LastQuotaUpdate,
&folder.Name) &folder.Name, &description)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return folder, &RecordNotFoundError{err: err.Error()} return folder, &RecordNotFoundError{err: err.Error()}
} }
if mappedPath.Valid { if mappedPath.Valid {
folder.MappedPath = mappedPath.String folder.MappedPath = mappedPath.String
} }
if description.Valid {
folder.Description = description.String
}
return folder, err return folder, err
} }
@ -654,7 +663,7 @@ func sqlCommonAddFolder(folder *vfs.BaseVirtualFolder, dbHandle sqlQuerier) erro
} }
defer stmt.Close() defer stmt.Close()
_, err = stmt.ExecContext(ctx, folder.MappedPath, folder.UsedQuotaSize, folder.UsedQuotaFiles, _, err = stmt.ExecContext(ctx, folder.MappedPath, folder.UsedQuotaSize, folder.UsedQuotaFiles,
folder.LastQuotaUpdate, folder.Name) folder.LastQuotaUpdate, folder.Name, folder.Description)
return err return err
} }
@ -672,7 +681,7 @@ func sqlCommonUpdateFolder(folder *vfs.BaseVirtualFolder, dbHandle *sql.DB) erro
return err return err
} }
defer stmt.Close() defer stmt.Close()
_, err = stmt.ExecContext(ctx, folder.MappedPath, folder.Name) _, err = stmt.ExecContext(ctx, folder.MappedPath, folder.Description, folder.Name)
return err return err
} }
@ -708,15 +717,18 @@ func sqlCommonDumpFolders(dbHandle sqlQuerier) ([]vfs.BaseVirtualFolder, error)
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var folder vfs.BaseVirtualFolder var folder vfs.BaseVirtualFolder
var mappedPath sql.NullString var mappedPath, description sql.NullString
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
&folder.LastQuotaUpdate, &folder.Name) &folder.LastQuotaUpdate, &folder.Name, &description)
if err != nil { if err != nil {
return folders, err return folders, err
} }
if mappedPath.Valid { if mappedPath.Valid {
folder.MappedPath = mappedPath.String folder.MappedPath = mappedPath.String
} }
if description.Valid {
folder.Description = description.String
}
folders = append(folders, folder) folders = append(folders, folder)
} }
err = rows.Err() err = rows.Err()
@ -745,15 +757,18 @@ func sqlCommonGetFolders(limit, offset int, order string, dbHandle sqlQuerier) (
defer rows.Close() defer rows.Close()
for rows.Next() { for rows.Next() {
var folder vfs.BaseVirtualFolder var folder vfs.BaseVirtualFolder
var mappedPath sql.NullString var mappedPath, description sql.NullString
err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles, err = rows.Scan(&folder.ID, &mappedPath, &folder.UsedQuotaSize, &folder.UsedQuotaFiles,
&folder.LastQuotaUpdate, &folder.Name) &folder.LastQuotaUpdate, &folder.Name, &description)
if err != nil { if err != nil {
return folders, err return folders, err
} }
if mappedPath.Valid { if mappedPath.Valid {
folder.MappedPath = mappedPath.String folder.MappedPath = mappedPath.String
} }
if description.Valid {
folder.Description = description.String
}
folders = append(folders, folder) folders = append(folders, folder)
} }

View file

@ -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_folder_id_idx" ON "{{folders_mapping}}" ("folder_id");
CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id"); CREATE INDEX "folders_mapping_user_id_idx" ON "{{folders_mapping}}" ("user_id");
INSERT INTO {{schema_version}} (version) VALUES (8); 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) providerLog(logger.LevelError, "%v", err)
logger.ErrorToConsole("%v", err) logger.ErrorToConsole("%v", err)
return err return err
case version == 8:
return updateSQLiteDatabaseFromV8(p.dbHandle)
default: default:
if version > sqlDatabaseVersion { if version > sqlDatabaseVersion {
providerLog(logger.LevelWarn, "database version %v is newer than the supported one: %v", version, 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 { if dbVersion.Version == targetVersion {
return errors.New("current version match target version, nothing to do") 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
} }

View file

@ -10,9 +10,10 @@ import (
const ( const (
selectUserFields = "id,username,password,public_keys,home_dir,uid,gid,max_sessions,quota_size,quota_files,permissions,used_quota_size," + 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" "used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,expiration_date,last_login,status,filters,filesystem," +
selectFolderFields = "id,path,used_quota_size,used_quota_files,last_quota_update,name" "additional_info,description"
selectAdminFields = "id,username,password,status,email,permissions,filters,additional_info" 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 { func getSQLPlaceholders() []string {
@ -41,15 +42,15 @@ func getDumpAdminsQuery() string {
} }
func getAddAdminQuery() string { func getAddAdminQuery() string {
return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info) return fmt.Sprintf(`INSERT INTO %v (username,password,status,email,permissions,filters,additional_info,description)
VALUES (%v,%v,%v,%v,%v,%v,%v)`, sqlTableAdmins, sqlPlaceholders[0], sqlPlaceholders[1], 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[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7])
} }
func getUpdateAdminQuery() string { 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], 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 { func getDeleteAdminQuery() string {
@ -94,20 +95,20 @@ func getQuotaQuery() string {
func getAddUserQuery() 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, 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, used_quota_size,used_quota_files,last_quota_update,upload_bandwidth,download_bandwidth,status,last_login,expiration_date,filters,
filesystem,additional_info) 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)`, sqlTableUsers, sqlPlaceholders[0], sqlPlaceholders[1], 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[2], sqlPlaceholders[3], sqlPlaceholders[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7],
sqlPlaceholders[8], sqlPlaceholders[9], sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], 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 { 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, 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, 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[4], sqlPlaceholders[5], sqlPlaceholders[6], sqlPlaceholders[7], sqlPlaceholders[8], sqlPlaceholders[9],
sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14], sqlPlaceholders[15], sqlPlaceholders[10], sqlPlaceholders[11], sqlPlaceholders[12], sqlPlaceholders[13], sqlPlaceholders[14], sqlPlaceholders[15],
sqlPlaceholders[16]) sqlPlaceholders[16], sqlPlaceholders[17])
} }
func getDeleteUserQuery() string { func getDeleteUserQuery() string {
@ -119,12 +120,14 @@ func getFolderByNameQuery() string {
} }
func getAddFolderQuery() 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)`, 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]) sqlTableFolders, sqlPlaceholders[0], sqlPlaceholders[1], sqlPlaceholders[2], sqlPlaceholders[3], sqlPlaceholders[4],
sqlPlaceholders[5])
} }
func getUpdateFolderQuery() string { 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 { func getDeleteFolderQuery() string {

View file

@ -219,6 +219,8 @@ type User struct {
Filters UserFilters `json:"filters"` Filters UserFilters `json:"filters"`
// Filesystem configuration details // Filesystem configuration details
FsConfig Filesystem `json:"filesystem"` FsConfig Filesystem `json:"filesystem"`
// optional description, for example full name
Description string `json:"description,omitempty"`
// free form text field for external systems // free form text field for external systems
AdditionalInfo string `json:"additional_info,omitempty"` AdditionalInfo string `json:"additional_info,omitempty"`
} }
@ -940,6 +942,7 @@ func (u *User) getACopy() User {
Filters: filters, Filters: filters,
FsConfig: fsConfig, FsConfig: fsConfig,
AdditionalInfo: u.AdditionalInfo, AdditionalInfo: u.AdditionalInfo,
Description: u.Description,
} }
} }

View file

@ -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. ... 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`. If you prefer GitHub Container Registry to Docker Hub replace `drakkan/sftpgo:tag` with `ghcr.io/drakkan/sftpgo:tag`.

View file

@ -2033,8 +2033,9 @@ func TestStartQuotaScan(t *testing.T) {
_, err = httpdtest.RemoveUser(user, http.StatusOK) _, err = httpdtest.RemoveUser(user, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
folder := vfs.BaseVirtualFolder{ folder := vfs.BaseVirtualFolder{
Name: "vfolder", Name: "vfolder",
MappedPath: filepath.Join(os.TempDir(), "folder"), MappedPath: filepath.Join(os.TempDir(), "folder"),
Description: "virtual folder",
} }
_, _, err = httpdtest.AddFolder(folder, http.StatusCreated) _, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -2444,9 +2445,11 @@ func TestFolders(t *testing.T) {
_, _, err = httpdtest.UpdateFolder(folder1, http.StatusBadRequest) _, _, err = httpdtest.UpdateFolder(folder1, http.StatusBadRequest)
assert.NoError(t, err) assert.NoError(t, err)
folder1.MappedPath = filepath.Join(os.TempDir(), "updated") folder1.MappedPath = filepath.Join(os.TempDir(), "updated")
folder1.Description = "updated folder description"
f, _, err = httpdtest.UpdateFolder(folder1, http.StatusOK) f, _, err = httpdtest.UpdateFolder(folder1, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, folder1.MappedPath, f.MappedPath) assert.Equal(t, folder1.MappedPath, f.MappedPath)
assert.Equal(t, folder1.Description, f.Description)
_, err = httpdtest.RemoveFolder(folder1, http.StatusOK) _, err = httpdtest.RemoveFolder(folder1, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -2632,6 +2635,7 @@ func TestLoaddataFromPostBody(t *testing.T) {
func TestLoaddata(t *testing.T) { func TestLoaddata(t *testing.T) {
mappedPath := filepath.Join(os.TempDir(), "restored_folder") mappedPath := filepath.Join(os.TempDir(), "restored_folder")
folderName := filepath.Base(mappedPath) folderName := filepath.Base(mappedPath)
foldeDesc := "restored folder desc"
user := getTestUser() user := getTestUser()
user.ID = 1 user.ID = 1
user.Username = "test_user_restore" user.Username = "test_user_restore"
@ -2651,8 +2655,9 @@ func TestLoaddata(t *testing.T) {
Users: []string{"user"}, Users: []string{"user"},
}, },
{ {
MappedPath: mappedPath, MappedPath: mappedPath,
Name: folderName, Name: folderName,
Description: foldeDesc,
}, },
} }
backupContent, err := json.Marshal(backupData) backupContent, err := json.Marshal(backupData)
@ -2698,6 +2703,7 @@ func TestLoaddata(t *testing.T) {
assert.Equal(t, int64(123), folder.UsedQuotaSize) assert.Equal(t, int64(123), folder.UsedQuotaSize)
assert.Equal(t, 456, folder.UsedQuotaFiles) assert.Equal(t, 456, folder.UsedQuotaFiles)
assert.Equal(t, int64(789), folder.LastQuotaUpdate) assert.Equal(t, int64(789), folder.LastQuotaUpdate)
assert.Equal(t, foldeDesc, folder.Description)
assert.Len(t, folder.Users, 0) assert.Len(t, folder.Users, 0)
_, err = httpdtest.RemoveFolder(folder, http.StatusOK) _, err = httpdtest.RemoveFolder(folder, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -2765,7 +2771,9 @@ func TestLoaddataMode(t *testing.T) {
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
oldInfo := admin.AdditionalInfo oldInfo := admin.AdditionalInfo
oldDesc := admin.Description
admin.AdditionalInfo = "newInfo" admin.AdditionalInfo = "newInfo"
admin.Description = "newDesc"
admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK) admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -2797,6 +2805,7 @@ func TestLoaddataMode(t *testing.T) {
admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
assert.NotEqual(t, oldInfo, admin.AdditionalInfo) assert.NotEqual(t, oldInfo, admin.AdditionalInfo)
assert.NotEqual(t, oldDesc, admin.Description)
_, _, err = httpdtest.Loaddata(backupFilePath, "0", "2", http.StatusOK) _, _, err = httpdtest.Loaddata(backupFilePath, "0", "2", http.StatusOK)
assert.NoError(t, err) assert.NoError(t, err)
@ -4183,6 +4192,7 @@ func TestWebAdminBasicMock(t *testing.T) {
form.Set("password", "") form.Set("password", "")
form.Set("status", "1") form.Set("status", "1")
form.Set("permissions", "*") form.Set("permissions", "*")
form.Set("description", admin.Description)
req, _ := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode()))) req, _ := http.NewRequest(http.MethodPost, webAdminPath, bytes.NewBuffer([]byte(form.Encode())))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
setJWTCookieForReq(req, token) setJWTCookieForReq(req, token)
@ -4525,6 +4535,7 @@ func TestWebUserAddMock(t *testing.T) {
user.DownloadBandwidth = 64 user.DownloadBandwidth = 64
user.UID = 1000 user.UID = 1000
user.AdditionalInfo = "info" user.AdditionalInfo = "info"
user.Description = "user dsc"
mappedDir := filepath.Join(os.TempDir(), "mapped") mappedDir := filepath.Join(os.TempDir(), "mapped")
folderName := filepath.Base(mappedDir) folderName := filepath.Base(mappedDir)
f := vfs.BaseVirtualFolder{ f := vfs.BaseVirtualFolder{
@ -4553,6 +4564,7 @@ func TestWebUserAddMock(t *testing.T) {
form.Set("allowed_patterns", "/dir2::*.jpg,*.png\n/dir1::*.png") form.Set("allowed_patterns", "/dir2::*.jpg,*.png\n/dir1::*.png")
form.Set("denied_patterns", "/dir1::*.zip\n/dir3::*.rar\n/dir2::*.mkv") form.Set("denied_patterns", "/dir1::*.zip\n/dir3::*.rar\n/dir2::*.mkv")
form.Set("additional_info", user.AdditionalInfo) form.Set("additional_info", user.AdditionalInfo)
form.Set("description", user.Description)
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
// test invalid url escape // test invalid url escape
req, _ = http.NewRequest(http.MethodPost, webUserPath+"?a=%2", &b) 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, user.DownloadBandwidth, newUser.DownloadBandwidth)
assert.Equal(t, int64(1000), newUser.Filters.MaxUploadFileSize) assert.Equal(t, int64(1000), newUser.Filters.MaxUploadFileSize)
assert.Equal(t, user.AdditionalInfo, newUser.AdditionalInfo) assert.Equal(t, user.AdditionalInfo, newUser.AdditionalInfo)
assert.Equal(t, user.Description, newUser.Description)
assert.True(t, utils.IsStringInSlice(testPubKey, newUser.PublicKeys)) assert.True(t, utils.IsStringInSlice(testPubKey, newUser.PublicKeys))
if val, ok := newUser.Permissions["/subdir"]; ok { if val, ok := newUser.Permissions["/subdir"]; ok {
assert.True(t, utils.IsStringInSlice(dataprovider.PermListItems, val)) 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("max_upload_file_size", "100")
form.Set("disconnect", "1") form.Set("disconnect", "1")
form.Set("additional_info", user.AdditionalInfo) form.Set("additional_info", user.AdditionalInfo)
form.Set("description", user.Description)
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b) req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
@ -4873,6 +4887,7 @@ func TestWebUserUpdateMock(t *testing.T) {
assert.Equal(t, user.UID, updateUser.UID) assert.Equal(t, user.UID, updateUser.UID)
assert.Equal(t, user.GID, updateUser.GID) assert.Equal(t, user.GID, updateUser.GID)
assert.Equal(t, user.AdditionalInfo, updateUser.AdditionalInfo) assert.Equal(t, user.AdditionalInfo, updateUser.AdditionalInfo)
assert.Equal(t, user.Description, updateUser.Description)
assert.Equal(t, int64(100), updateUser.Filters.MaxUploadFileSize) assert.Equal(t, int64(100), updateUser.Filters.MaxUploadFileSize)
if val, ok := updateUser.Permissions["/otherdir"]; ok { if val, ok := updateUser.Permissions["/otherdir"]; ok {
@ -4903,8 +4918,9 @@ func TestRenderFolderTemplateMock(t *testing.T) {
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
folder := vfs.BaseVirtualFolder{ folder := vfs.BaseVirtualFolder{
Name: "templatefolder", Name: "templatefolder",
MappedPath: filepath.Join(os.TempDir(), "mapped"), MappedPath: filepath.Join(os.TempDir(), "mapped"),
Description: "template folder desc",
} }
folder, _, err = httpdtest.AddFolder(folder, http.StatusCreated) folder, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
assert.NoError(t, err) assert.NoError(t, err)
@ -4977,8 +4993,9 @@ func TestRenderWebCloneUserMock(t *testing.T) {
func TestUserTemplateWithFoldersMock(t *testing.T) { func TestUserTemplateWithFoldersMock(t *testing.T) {
folder := vfs.BaseVirtualFolder{ folder := vfs.BaseVirtualFolder{
Name: "vfolder", Name: "vfolder",
MappedPath: filepath.Join(os.TempDir(), "mapped"), MappedPath: filepath.Join(os.TempDir(), "mapped"),
Description: "vfolder desc with spéciàl ch@rs",
} }
token, err := getJWTWebTokenFromTestServer(defaultTokenAuthUser, defaultTokenAuthPass) 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("expiration_date", "2020-01-01 00:00:00")
form.Set("fs_provider", "0") form.Set("fs_provider", "0")
form.Set("max_upload_file_size", "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("virtual_folders", "/vdir%username%::"+folder.Name+"::-1::-1")
form.Set("users", "auser1::password1\nauser2::password2::"+testPubKey+"\nauser1::password") form.Set("users", "auser1::password1\nauser2::password2::"+testPubKey+"\nauser1::password")
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
@ -5041,10 +5059,13 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
folder1 := dump.Folders[0] folder1 := dump.Folders[0]
assert.Equal(t, "auser1", user1.Username) assert.Equal(t, "auser1", user1.Username)
assert.Equal(t, "auser2", user2.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(), user1.Username), user1.HomeDir)
assert.Equal(t, filepath.Join(os.TempDir(), user2.Username), user2.HomeDir) assert.Equal(t, filepath.Join(os.TempDir(), user2.Username), user2.HomeDir)
assert.Equal(t, folder.Name, folder1.Name) assert.Equal(t, folder.Name, folder1.Name)
assert.Equal(t, folder.MappedPath, folder1.MappedPath) assert.Equal(t, folder.MappedPath, folder1.MappedPath)
assert.Equal(t, folder.Description, folder1.Description)
assert.Len(t, user1.PublicKeys, 0) assert.Len(t, user1.PublicKeys, 0)
assert.Len(t, user2.PublicKeys, 1) assert.Len(t, user2.PublicKeys, 1)
assert.Len(t, user1.VirtualFolders, 1) assert.Len(t, user1.VirtualFolders, 1)
@ -5178,6 +5199,7 @@ func TestFolderTemplateMock(t *testing.T) {
form := make(url.Values) form := make(url.Values)
form.Set("name", folderName) form.Set("name", folderName)
form.Set("mapped_path", mappedPath) form.Set("mapped_path", mappedPath)
form.Set("description", "desc folder %name%")
form.Set("folders", "folder1\nfolder2\nfolder3\nfolder1\n\n\n") form.Set("folders", "folder1\nfolder2\nfolder3\nfolder1\n\n\n")
contentType := "application/x-www-form-urlencoded" contentType := "application/x-www-form-urlencoded"
req, _ := http.NewRequest(http.MethodPost, webTemplateFolder, bytes.NewBuffer([]byte(form.Encode()))) 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.Admins, 0)
require.Len(t, dump.Folders, 3) require.Len(t, dump.Folders, 3)
require.Equal(t, "folder1", dump.Folders[0].Name) 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.True(t, strings.HasSuffix(dump.Folders[0].MappedPath, "folder1mappedfolder1path"))
require.Equal(t, "folder2", dump.Folders[1].Name) 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.True(t, strings.HasSuffix(dump.Folders[1].MappedPath, "folder2mappedfolder2path"))
require.Equal(t, "folder3", dump.Folders[2].Name) 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")) require.True(t, strings.HasSuffix(dump.Folders[2].MappedPath, "folder3mappedfolder3path"))
form.Set("folders", "\n\n\n") form.Set("folders", "\n\n\n")
@ -5258,6 +5283,7 @@ func TestWebUserS3Mock(t *testing.T) {
user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/" user.FsConfig.S3Config.KeyPrefix = "somedir/subdir/"
user.FsConfig.S3Config.UploadPartSize = 5 user.FsConfig.S3Config.UploadPartSize = 5
user.FsConfig.S3Config.UploadConcurrency = 4 user.FsConfig.S3Config.UploadConcurrency = 4
user.Description = "s3 tèst user"
form := make(url.Values) form := make(url.Values)
form.Set(csrfFormToken, csrfToken) form.Set(csrfFormToken, csrfToken)
form.Set("username", user.Username) form.Set("username", user.Username)
@ -5287,6 +5313,7 @@ func TestWebUserS3Mock(t *testing.T) {
form.Set("allowed_extensions", "/dir1::.jpg,.png") form.Set("allowed_extensions", "/dir1::.jpg,.png")
form.Set("denied_extensions", "/dir2::.zip") form.Set("denied_extensions", "/dir2::.zip")
form.Set("max_upload_file_size", "0") form.Set("max_upload_file_size", "0")
form.Set("description", user.Description)
// test invalid s3_upload_part_size // test invalid s3_upload_part_size
form.Set("s3_upload_part_size", "a") form.Set("s3_upload_part_size", "a")
b, contentType, _ := getMultipartFormData(form, "", "") b, contentType, _ := getMultipartFormData(form, "", "")
@ -5333,6 +5360,7 @@ func TestWebUserS3Mock(t *testing.T) {
assert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload()) assert.NotEmpty(t, updateUser.FsConfig.S3Config.AccessSecret.GetPayload())
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey()) assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetKey())
assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData()) assert.Empty(t, updateUser.FsConfig.S3Config.AccessSecret.GetAdditionalData())
assert.Equal(t, user.Description, updateUser.Description)
// now check that a redacted password is not saved // now check that a redacted password is not saved
form.Set("s3_access_secret", redactedSecret) form.Set("s3_access_secret", redactedSecret)
b, contentType, _ = getMultipartFormData(form, "", "") b, contentType, _ = getMultipartFormData(form, "", "")
@ -5819,9 +5847,11 @@ func TestAddWebFoldersMock(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
mappedPath := filepath.Clean(os.TempDir()) mappedPath := filepath.Clean(os.TempDir())
folderName := filepath.Base(mappedPath) folderName := filepath.Base(mappedPath)
folderDesc := "a simple desc"
form := make(url.Values) form := make(url.Values)
form.Set("mapped_path", mappedPath) form.Set("mapped_path", mappedPath)
form.Set("name", folderName) form.Set("name", folderName)
form.Set("description", folderDesc)
req, err := http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode())) req, err := http.NewRequest(http.MethodPost, webFolderPath, strings.NewReader(form.Encode()))
assert.NoError(t, err) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
@ -5868,6 +5898,7 @@ func TestAddWebFoldersMock(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, mappedPath, folder.MappedPath) assert.Equal(t, mappedPath, folder.MappedPath)
assert.Equal(t, folderName, folder.Name) assert.Equal(t, folderName, folder.Name)
assert.Equal(t, folderDesc, folder.Description)
// cleanup // cleanup
req, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil) req, _ = http.NewRequest(http.MethodDelete, path.Join(folderPath, folderName), nil)
setBearerForReq(req, apiToken) setBearerForReq(req, apiToken)
@ -5883,9 +5914,11 @@ func TestUpdateWebFolderMock(t *testing.T) {
csrfToken, err := getCSRFToken() csrfToken, err := getCSRFToken()
assert.NoError(t, err) assert.NoError(t, err)
folderName := "vfolderupdate" folderName := "vfolderupdate"
folderDesc := "updated desc"
folder := vfs.BaseVirtualFolder{ folder := vfs.BaseVirtualFolder{
Name: folderName, Name: folderName,
MappedPath: filepath.Join(os.TempDir(), "folderupdate"), MappedPath: filepath.Join(os.TempDir(), "folderupdate"),
Description: "dsc",
} }
_, _, err = httpdtest.AddFolder(folder, http.StatusCreated) _, _, err = httpdtest.AddFolder(folder, http.StatusCreated)
newMappedPath := folder.MappedPath + "1" newMappedPath := folder.MappedPath + "1"
@ -5893,6 +5926,7 @@ func TestUpdateWebFolderMock(t *testing.T) {
form := make(url.Values) form := make(url.Values)
form.Set("mapped_path", newMappedPath) form.Set("mapped_path", newMappedPath)
form.Set("name", folderName) form.Set("name", folderName)
form.Set("description", folderDesc)
form.Set(csrfFormToken, "") form.Set(csrfFormToken, "")
req, err := http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), strings.NewReader(form.Encode())) req, err := http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName), strings.NewReader(form.Encode()))
assert.NoError(t, err) assert.NoError(t, err)
@ -5910,6 +5944,16 @@ func TestUpdateWebFolderMock(t *testing.T) {
rr = executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusSeeOther, rr) 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 // parse form error
req, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName)+"??a=a%B3%A2%G3", strings.NewReader(form.Encode())) req, err = http.NewRequest(http.MethodPost, path.Join(webFolderPath, folderName)+"??a=a%B3%A2%G3", strings.NewReader(form.Encode()))
assert.NoError(t, err) assert.NoError(t, err)
@ -5976,29 +6020,57 @@ func TestWebFoldersMock(t *testing.T) {
mappedPath2 := filepath.Join(os.TempDir(), "vfolder2") mappedPath2 := filepath.Join(os.TempDir(), "vfolder2")
folderName1 := filepath.Base(mappedPath1) folderName1 := filepath.Base(mappedPath1)
folderName2 := filepath.Base(mappedPath2) folderName2 := filepath.Base(mappedPath2)
folderDesc1 := "vfolder1 desc"
folderDesc2 := "vfolder2 desc"
folders := []vfs.BaseVirtualFolder{ folders := []vfs.BaseVirtualFolder{
{ {
Name: folderName1, Name: folderName1,
MappedPath: mappedPath1, MappedPath: mappedPath1,
Description: folderDesc1,
}, },
{ {
Name: folderName2, Name: folderName2,
MappedPath: mappedPath2, MappedPath: mappedPath2,
Description: folderDesc2,
}, },
} }
for _, folder := range folders { for _, folder := range folders {
folderAsJSON, err := json.Marshal(folder) folderAsJSON, err := json.Marshal(folder)
assert.NoError(t, err) 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) setBearerForReq(req, apiToken)
rr := executeRequest(req) rr := executeRequest(req)
checkResponseCode(t, http.StatusCreated, rr) 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) assert.NoError(t, err)
setJWTCookieForReq(req, webToken) setJWTCookieForReq(req, webToken)
rr := executeRequest(req) rr = executeRequest(req)
checkResponseCode(t, http.StatusOK, rr) checkResponseCode(t, http.StatusOK, rr)
req, err = http.NewRequest(http.MethodGet, webFoldersPath+"?qlimit=a", nil) req, err = http.NewRequest(http.MethodGet, webFoldersPath+"?qlimit=a", nil)
assert.NoError(t, err) assert.NoError(t, err)
@ -6145,15 +6217,17 @@ func getTestAdmin() dataprovider.Admin {
Status: 1, Status: 1,
Permissions: []string{dataprovider.PermAdminAny}, Permissions: []string{dataprovider.PermAdminAny},
Email: "admin@example.com", Email: "admin@example.com",
Description: "test admin",
} }
} }
func getTestUser() dataprovider.User { func getTestUser() dataprovider.User {
user := dataprovider.User{ user := dataprovider.User{
Username: defaultUsername, Username: defaultUsername,
Password: defaultPassword, Password: defaultPassword,
HomeDir: filepath.Join(homeBasePath, defaultUsername), HomeDir: filepath.Join(homeBasePath, defaultUsername),
Status: 1, Status: 1,
Description: "test user",
} }
user.Permissions = make(map[string][]string) user.Permissions = make(map[string][]string)
user.Permissions["/"] = defaultPerms user.Permissions["/"] = defaultPerms

View file

@ -2,7 +2,7 @@ openapi: 3.0.3
info: info:
title: SFTPGo title: SFTPGo
description: SFTPGo REST API description: SFTPGo REST API
version: 2.4.4 version: 2.4.5
servers: servers:
- url: /api/v2 - url: /api/v2
@ -1560,6 +1560,9 @@ components:
mapped_path: mapped_path:
type: string type: string
description: absolute filesystem path to use as virtual folder description: absolute filesystem path to use as virtual folder
description:
type: string
description: optional description
used_quota_size: used_quota_size:
type: integer type: integer
format: int64 format: int64
@ -1575,8 +1578,6 @@ components:
items: items:
type: string type: string
description: list of usernames associated with this virtual folder 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. 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: VirtualFolder:
allOf: allOf:
@ -1615,6 +1616,9 @@ components:
username: username:
type: string type: string
description: username is unique description: username is unique
description:
type: string
description: optional description, for example the user full name
expiration_date: expiration_date:
type: integer type: integer
format: int64 format: int64
@ -1723,6 +1727,9 @@ components:
username: username:
type: string type: string
description: username is unique description: username is unique
description:
type: string
description: optional description, for example the admin full name
password: password:
type: string type: string
format: password format: password

View file

@ -802,6 +802,7 @@ func getAdminFromPostFields(r *http.Request) (dataprovider.Admin, error) {
admin.Status = status admin.Status = status
admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",") admin.Filters.AllowList = getSliceFromDelimitedValues(r.Form.Get("allowed_ip"), ",")
admin.AdditionalInfo = r.Form.Get("additional_info") admin.AdditionalInfo = r.Form.Get("additional_info")
admin.Description = r.Form.Get("description")
return admin, nil return admin, nil
} }
@ -818,6 +819,7 @@ func getFolderFromTemplate(folder vfs.BaseVirtualFolder, name string) vfs.BaseVi
replacements["%name%"] = folder.Name replacements["%name%"] = folder.Name
folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements) folder.MappedPath = replacePlaceholders(folder.MappedPath, replacements)
folder.Description = replacePlaceholders(folder.Description, replacements)
return folder return folder
} }
@ -887,6 +889,7 @@ func getUserFromTemplate(user dataprovider.User, template userTemplateFields) da
vfolders = append(vfolders, vfolder) vfolders = append(vfolders, vfolder)
} }
user.VirtualFolders = vfolders user.VirtualFolders = vfolders
user.Description = replacePlaceholders(user.Description, replacements)
user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements) user.AdditionalInfo = replacePlaceholders(user.AdditionalInfo, replacements)
switch user.FsConfig.Provider { switch user.FsConfig.Provider {
@ -977,6 +980,7 @@ func getUserFromPostFields(r *http.Request) (dataprovider.User, error) {
Filters: getFiltersFromUserPostFields(r), Filters: getFiltersFromUserPostFields(r),
FsConfig: fsConfig, FsConfig: fsConfig,
AdditionalInfo: r.Form.Get("additional_info"), AdditionalInfo: r.Form.Get("additional_info"),
Description: r.Form.Get("description"),
} }
maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64) maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64)
user.Filters.MaxUploadFileSize = maxFileSize user.Filters.MaxUploadFileSize = maxFileSize
@ -1250,6 +1254,7 @@ func handleWebTemplateFolderPost(w http.ResponseWriter, r *http.Request) {
} }
templateFolder.MappedPath = r.Form.Get("mapped_path") templateFolder.MappedPath = r.Form.Get("mapped_path")
templateFolder.Description = r.Form.Get("description")
var dump dataprovider.BackupData var dump dataprovider.BackupData
dump.Version = dataprovider.DumpVersion dump.Version = dataprovider.DumpVersion
@ -1458,6 +1463,7 @@ func handleWebAddFolderPost(w http.ResponseWriter, r *http.Request) {
} }
folder.MappedPath = r.Form.Get("mapped_path") folder.MappedPath = r.Form.Get("mapped_path")
folder.Name = r.Form.Get("name") folder.Name = r.Form.Get("name")
folder.Description = r.Form.Get("description")
err = dataprovider.AddFolder(&folder) err = dataprovider.AddFolder(&folder)
if err == nil { if err == nil {
@ -1501,6 +1507,7 @@ func handleWebUpdateFolderPost(w http.ResponseWriter, r *http.Request) {
return return
} }
folder.MappedPath = r.Form.Get("mapped_path") folder.MappedPath = r.Form.Get("mapped_path")
folder.Description = r.Form.Get("description")
err = dataprovider.UpdateFolder(&folder) err = dataprovider.UpdateFolder(&folder)
if err != nil { if err != nil {
renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error()) renderFolderPage(w, r, folder, folderPageModeUpdate, err.Error())

View file

@ -845,6 +845,9 @@ func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder)
if expected.UsedQuotaFiles != actual.UsedQuotaFiles { if expected.UsedQuotaFiles != actual.UsedQuotaFiles {
return errors.New("used quota files mismatch") return errors.New("used quota files mismatch")
} }
if expected.Description != actual.Description {
return errors.New("Description mismatch")
}
if len(expected.Users) != len(actual.Users) { if len(expected.Users) != len(actual.Users) {
return errors.New("folder users mismatch") 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") return errors.New("admin ID mismatch")
} }
} }
if expected.Username != actual.Username { if err := compareAdminEqualFields(expected, actual); err != nil {
return errors.New("Username mismatch") return err
}
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 len(expected.Permissions) != len(actual.Permissions) { if len(expected.Permissions) != len(actual.Permissions) {
return errors.New("Permissions mismatch") return errors.New("Permissions mismatch")
@ -901,6 +895,25 @@ func checkAdmin(expected *dataprovider.Admin, actual *dataprovider.Admin) error
return nil 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 { func checkUser(expected *dataprovider.User, actual *dataprovider.User) error {
if actual.Password != "" { if actual.Password != "" {
return errors.New("User password must not be visible") 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 { if expected.AdditionalInfo != actual.AdditionalInfo {
return errors.New("AdditionalInfo mismatch") return errors.New("AdditionalInfo mismatch")
} }
if expected.Description != actual.Description {
return errors.New("Description mismatch")
}
return nil return nil
} }

View file

@ -257,7 +257,7 @@ func (s *Service) loadInitialData() error {
if err != nil { if err != nil {
return fmt.Errorf("unable to parse file to restore %#v: %v", s.LoadDataFrom, err) 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 { if err != nil {
return err return err
} }
@ -278,7 +278,7 @@ func (s *Service) loadInitialData() error {
return nil 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) err := httpd.RestoreAdmins(dump.Admins, s.LoadDataFrom, s.LoadDataMode)
if err != nil { if err != nil {
return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err) return fmt.Errorf("unable to restore admins from file %#v: %v", s.LoadDataFrom, err)

View file

@ -22,6 +22,17 @@
</div> </div>
</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"> <div class="form-group row">
<label for="idStatus" class="col-sm-2 col-form-label">Status</label> <label for="idStatus" class="col-sm-2 col-form-label">Status</label>
<div class="col-sm-10"> <div class="col-sm-10">

View file

@ -49,6 +49,16 @@
</div> </div>
</div> </div>
{{end}} {{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"> <div class="form-group row">
<label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label> <label for="idMappedPath" class="col-sm-2 col-form-label">Absolute Path</label>
<div class="col-sm-10"> <div class="col-sm-10">

View file

@ -61,6 +61,17 @@
</div> </div>
{{end}} {{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"> <div class="form-group row">
<label for="idStatus" class="col-sm-2 col-form-label">Status</label> <label for="idStatus" class="col-sm-2 col-form-label">Status</label>
<div class="col-sm-10"> <div class="col-sm-10">

View file

@ -2,7 +2,7 @@ package version
import "strings" import "strings"
const version = "2.0.90-dev" const version = "2.0.2-dev"
var ( var (
commit = "" commit = ""

View file

@ -15,6 +15,7 @@ type BaseVirtualFolder struct {
ID int64 `json:"id"` ID int64 `json:"id"`
Name string `json:"name"` Name string `json:"name"`
MappedPath string `json:"mapped_path,omitempty"` MappedPath string `json:"mapped_path,omitempty"`
Description string `json:"description,omitempty"`
UsedQuotaSize int64 `json:"used_quota_size"` UsedQuotaSize int64 `json:"used_quota_size"`
// Used quota as number of files // Used quota as number of files
UsedQuotaFiles int `json:"used_quota_files"` UsedQuotaFiles int `json:"used_quota_files"`
@ -31,6 +32,7 @@ func (v *BaseVirtualFolder) GetACopy() BaseVirtualFolder {
return BaseVirtualFolder{ return BaseVirtualFolder{
ID: v.ID, ID: v.ID,
Name: v.Name, Name: v.Name,
Description: v.Description,
MappedPath: v.MappedPath, MappedPath: v.MappedPath,
UsedQuotaSize: v.UsedQuotaSize, UsedQuotaSize: v.UsedQuotaSize,
UsedQuotaFiles: v.UsedQuotaFiles, UsedQuotaFiles: v.UsedQuotaFiles,