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"`
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,
}
}

View file

@ -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 {

View file

@ -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$"

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_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)
}

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_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)
}

View file

@ -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)
}

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_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
}

View file

@ -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 {

View file

@ -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,
}
}

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.
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`.

View file

@ -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

View file

@ -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

View file

@ -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())

View file

@ -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
}

View file

@ -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)

View file

@ -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">

View file

@ -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">

View file

@ -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">

View file

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

View file

@ -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,