diff --git a/dataprovider/admin.go b/dataprovider/admin.go index b648544e..86eba738 100644 --- a/dataprovider/admin.go +++ b/dataprovider/admin.go @@ -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, } } diff --git a/dataprovider/bolt.go b/dataprovider/bolt.go index db3225f5..88d1af41 100644 --- a/dataprovider/bolt.go +++ b/dataprovider/bolt.go @@ -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 { diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index 702c6534..1361c8dc 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -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$" diff --git a/dataprovider/mysql.go b/dataprovider/mysql.go index ce3b9dda..de621f8c 100644 --- a/dataprovider/mysql.go +++ b/dataprovider/mysql.go @@ -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) } diff --git a/dataprovider/pgsql.go b/dataprovider/pgsql.go index aa5674ad..997f93c1 100644 --- a/dataprovider/pgsql.go +++ b/dataprovider/pgsql.go @@ -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) } diff --git a/dataprovider/sqlcommon.go b/dataprovider/sqlcommon.go index 398dd3a4..92936124 100644 --- a/dataprovider/sqlcommon.go +++ b/dataprovider/sqlcommon.go @@ -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) } diff --git a/dataprovider/sqlite.go b/dataprovider/sqlite.go index c9acf9c0..87292398 100644 --- a/dataprovider/sqlite.go +++ b/dataprovider/sqlite.go @@ -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 } diff --git a/dataprovider/sqlqueries.go b/dataprovider/sqlqueries.go index fc1bc0a4..86348c04 100644 --- a/dataprovider/sqlqueries.go +++ b/dataprovider/sqlqueries.go @@ -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 { diff --git a/dataprovider/user.go b/dataprovider/user.go index 6ac8be7c..14c675b8 100644 --- a/dataprovider/user.go +++ b/dataprovider/user.go @@ -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, } } diff --git a/docker/README.md b/docker/README.md index 917f10e2..edcff071 100644 --- a/docker/README.md +++ b/docker/README.md @@ -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`. diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index 79284747..cc9dcc5a 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -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 diff --git a/httpd/schema/openapi.yaml b/httpd/schema/openapi.yaml index 280480fc..5293b300 100644 --- a/httpd/schema/openapi.yaml +++ b/httpd/schema/openapi.yaml @@ -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 diff --git a/httpd/web.go b/httpd/web.go index 13c34cf0..1cfe43c1 100644 --- a/httpd/web.go +++ b/httpd/web.go @@ -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()) diff --git a/httpdtest/httpdtest.go b/httpdtest/httpdtest.go index 3ef524fd..e863c5a9 100644 --- a/httpdtest/httpdtest.go +++ b/httpdtest/httpdtest.go @@ -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 } diff --git a/service/service.go b/service/service.go index c1b80a86..dcacc0f1 100644 --- a/service/service.go +++ b/service/service.go @@ -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) diff --git a/templates/admin.html b/templates/admin.html index 4b74f072..359ddbd3 100644 --- a/templates/admin.html +++ b/templates/admin.html @@ -22,6 +22,17 @@ +
+ +
+ + + Optional description, for example the admin full name + +
+
+
diff --git a/templates/folder.html b/templates/folder.html index b6aa0675..bbfff954 100644 --- a/templates/folder.html +++ b/templates/folder.html @@ -49,6 +49,16 @@
{{end}} +
+ +
+ + + Optional description + +
+
diff --git a/templates/user.html b/templates/user.html index 22b3aaf4..cb042dfe 100644 --- a/templates/user.html +++ b/templates/user.html @@ -61,6 +61,17 @@
{{end}} +
+ +
+ + + Optional description, for example the user full name + +
+
+
diff --git a/version/version.go b/version/version.go index cdf0d332..1c135fca 100644 --- a/version/version.go +++ b/version/version.go @@ -2,7 +2,7 @@ package version import "strings" -const version = "2.0.90-dev" +const version = "2.0.2-dev" var ( commit = "" diff --git a/vfs/folder.go b/vfs/folder.go index 7db4345c..cf0b04f9 100644 --- a/vfs/folder.go +++ b/vfs/folder.go @@ -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,