From 0a3d1642f757921a796b087cd97723573c7fd6be Mon Sep 17 00:00:00 2001 From: Jon_K Date: Sat, 3 Jan 2026 01:07:26 -0500 Subject: [PATCH] Add user quota management system Introduces comprehensive quota management with real-time usage tracking, frontend components, backend API endpoints, and TUS upload integration for storage limit enforcement. --- cmd/users.go | 17 ++ cmd/users_update.go | 20 +- errors/errors.go | 2 + frontend/src/api/tus.ts | 34 ++- frontend/src/api/users.ts | 4 + .../src/components/settings/QuotaInput.vue | 207 ++++++++++++++++++ .../src/components/settings/QuotaUsage.vue | 174 +++++++++++++++ frontend/src/components/settings/UserForm.vue | 11 + frontend/src/i18n/en.json | 12 +- frontend/src/types/user.d.ts | 15 ++ frontend/src/views/settings/User.vue | 4 + http/http.go | 1 + http/quota.go | 65 ++++++ http/resource.go | 8 +- http/tus_handlers.go | 8 +- http/users.go | 20 +- settings/defaults.go | 6 + users/quota.go | 161 ++++++++++++++ users/users.go | 19 ++ 19 files changed, 764 insertions(+), 24 deletions(-) create mode 100644 frontend/src/components/settings/QuotaInput.vue create mode 100644 frontend/src/components/settings/QuotaUsage.vue create mode 100644 http/quota.go create mode 100644 users/quota.go diff --git a/cmd/users.go b/cmd/users.go index 86434a42..c53ef379 100644 --- a/cmd/users.go +++ b/cmd/users.go @@ -80,6 +80,9 @@ func addUserFlags(flags *pflag.FlagSet) { flags.Bool("dateFormat", false, "use date format (true for absolute time, false for relative)") flags.Bool("hideDotfiles", false, "hide dotfiles") flags.String("aceEditorTheme", "", "ace editor's syntax highlighting theme for users") + flags.Float64("quota.limit", 0, "quota limit in KB, MB, GB or TB (0 = unlimited)") + flags.String("quota.unit", "GB", "quota unit (KB, MB, GB or TB)") + flags.Bool("quota.enforce", false, "enforce quota (hard limit)") } func getAndParseViewMode(flags *pflag.FlagSet) (users.ViewMode, error) { @@ -136,6 +139,20 @@ func getUserDefaults(flags *pflag.FlagSet, defaults *settings.UserDefaults, all defaults.Sorting.Asc, err = flags.GetBool(flag.Name) case "hideDotfiles": defaults.HideDotfiles, err = flags.GetBool(flag.Name) + case "quota.limit": + var quotaLimit float64 + quotaLimit, err = flags.GetFloat64(flag.Name) + if err == nil { + var quotaUnit string + quotaUnit, err = flags.GetString("quota.unit") + if err == nil { + defaults.QuotaLimit, err = users.ConvertToBytes(quotaLimit, quotaUnit) + } + } + case "quota.unit": + defaults.QuotaUnit, err = flags.GetString(flag.Name) + case "quota.enforce": + defaults.EnforceQuota, err = flags.GetBool(flag.Name) } if err != nil { diff --git a/cmd/users_update.go b/cmd/users_update.go index 96f1e2d3..69a3fc63 100644 --- a/cmd/users_update.go +++ b/cmd/users_update.go @@ -52,13 +52,16 @@ options you want to change.`, } defaults := settings.UserDefaults{ - Scope: user.Scope, - Locale: user.Locale, - ViewMode: user.ViewMode, - SingleClick: user.SingleClick, - Perm: user.Perm, - Sorting: user.Sorting, - Commands: user.Commands, + Scope: user.Scope, + Locale: user.Locale, + ViewMode: user.ViewMode, + SingleClick: user.SingleClick, + Perm: user.Perm, + Sorting: user.Sorting, + Commands: user.Commands, + QuotaLimit: user.QuotaLimit, + QuotaUnit: user.QuotaUnit, + EnforceQuota: user.EnforceQuota, } err = getUserDefaults(flags, &defaults, false) @@ -73,6 +76,9 @@ options you want to change.`, user.Perm = defaults.Perm user.Commands = defaults.Commands user.Sorting = defaults.Sorting + user.QuotaLimit = defaults.QuotaLimit + user.QuotaUnit = defaults.QuotaUnit + user.EnforceQuota = defaults.EnforceQuota user.LockPassword, err = flags.GetBool("lockPassword") if err != nil { return err diff --git a/errors/errors.go b/errors/errors.go index 85258e5b..83ee8bf6 100644 --- a/errors/errors.go +++ b/errors/errors.go @@ -22,6 +22,8 @@ var ( ErrInvalidRequestParams = errors.New("invalid request params") ErrSourceIsParent = errors.New("source is parent") ErrRootUserDeletion = errors.New("user with id 1 can't be deleted") + ErrQuotaExceeded = errors.New("quota exceeded") + ErrInvalidQuotaUnit = errors.New("invalid quota unit") ) type ErrShortPassword struct { diff --git a/frontend/src/api/tus.ts b/frontend/src/api/tus.ts index d6601166..47230cd2 100644 --- a/frontend/src/api/tus.ts +++ b/frontend/src/api/tus.ts @@ -47,6 +47,11 @@ export async function upload( return false; } + // Do not retry for quota exceeded (413 Request Entity Too Large) + if (status === 413) { + return false; + } + return true; }, onError: function (error: Error | tus.DetailedError) { @@ -56,12 +61,29 @@ export async function upload( return reject(error); } - const message = - error instanceof tus.DetailedError - ? error.originalResponse === null - ? "000 No connection" - : error.originalResponse.getBody() - : "Upload failed"; + let message = "Upload failed"; + + if (error instanceof tus.DetailedError) { + if (error.originalResponse === null) { + message = "000 No connection"; + } else { + const status = error.originalResponse.getStatus(); + + // Handle quota exceeded error (413) + if (status === 413) { + const quotaUsed = error.originalResponse.getHeader("X-Quota-Used"); + const quotaLimit = error.originalResponse.getHeader("X-Quota-Limit"); + + if (quotaUsed && quotaLimit) { + message = `Cannot upload file: storage quota exceeded. Current usage: ${quotaUsed}, Limit: ${quotaLimit}`; + } else { + message = "Cannot upload file: storage quota exceeded"; + } + } else { + message = error.originalResponse.getBody(); + } + } + } console.error(error); diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts index 78096b49..d68a5992 100644 --- a/frontend/src/api/users.ts +++ b/frontend/src/api/users.ts @@ -41,3 +41,7 @@ export async function remove(id: number) { method: "DELETE", }); } + +export async function getQuota(id: number) { + return fetchJSON(`/api/users/${id}/quota`, {}); +} diff --git a/frontend/src/components/settings/QuotaInput.vue b/frontend/src/components/settings/QuotaInput.vue new file mode 100644 index 00000000..37d1f345 --- /dev/null +++ b/frontend/src/components/settings/QuotaInput.vue @@ -0,0 +1,207 @@ + + + + + diff --git a/frontend/src/components/settings/QuotaUsage.vue b/frontend/src/components/settings/QuotaUsage.vue new file mode 100644 index 00000000..193da560 --- /dev/null +++ b/frontend/src/components/settings/QuotaUsage.vue @@ -0,0 +1,174 @@ + + + + + diff --git a/frontend/src/components/settings/UserForm.vue b/frontend/src/components/settings/UserForm.vue index c4f0e0c6..f93c9949 100644 --- a/frontend/src/components/settings/UserForm.vue +++ b/frontend/src/components/settings/UserForm.vue @@ -58,6 +58,13 @@ + +

{{ t("settings.rules") }}

{{ t("settings.rulesHelp") }}

@@ -71,11 +78,15 @@ import Languages from "./Languages.vue"; import Rules from "./Rules.vue"; import Permissions from "./Permissions.vue"; import Commands from "./Commands.vue"; +import QuotaInput from "./QuotaInput.vue"; import { enableExec } from "@/utils/constants"; import { computed, onMounted, ref, watch } from "vue"; import { useI18n } from "vue-i18n"; +import { useAuthStore } from "@/stores/auth"; const { t } = useI18n(); +const authStore = useAuthStore(); +const isLoggedInUserAdmin = computed(() => authStore.user?.perm.admin); const createUserDirData = ref(null); const originalUserScope = ref(null); diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index de5368bd..fc3eb2f3 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -61,7 +61,8 @@ "forbidden": "You don't have permissions to access this.", "internal": "Something really went wrong.", "notFound": "This location can't be reached.", - "connection": "The server can't be reached." + "connection": "The server can't be reached.", + "quotaExceeded": "Cannot upload file: storage quota exceeded. Current usage: {used}, Limit: {limit}" }, "files": { "body": "Body", @@ -258,7 +259,14 @@ "userManagement": "User Management", "userUpdated": "User updated!", "username": "Username", - "users": "Users" + "users": "Users", + "quota": "Storage Quota", + "quotaLimit": "Quota Limit", + "quotaUnit": "Unit", + "enforceQuota": "Enforce Quota (Hard Limit)", + "quotaUsage": "Quota Usage", + "quotaExceeded": "Storage quota exceeded", + "quotaWarning": "Approaching storage quota limit" }, "sidebar": { "help": "Help", diff --git a/frontend/src/types/user.d.ts b/frontend/src/types/user.d.ts index 40c453c5..4d166ed2 100644 --- a/frontend/src/types/user.d.ts +++ b/frontend/src/types/user.d.ts @@ -14,6 +14,10 @@ interface IUser { viewMode: ViewModeType; sorting?: Sorting; aceEditorTheme: string; + quotaLimit: number; + quotaUnit: string; + enforceQuota: boolean; + quotaUsed: number; } type ViewModeType = "list" | "mosaic" | "mosaic gallery"; @@ -31,6 +35,9 @@ interface IUserForm { hideDotfiles?: boolean; singleClick?: boolean; dateFormat?: boolean; + quotaLimit?: number; + quotaUnit?: string; + enforceQuota?: boolean; } interface Permissions { @@ -64,4 +71,12 @@ interface IRegexp { raw: string; } +interface IQuotaInfo { + limit: number; + used: number; + unit: string; + enforce: boolean; + percentage: number; +} + type UserTheme = "light" | "dark" | ""; diff --git a/frontend/src/views/settings/User.vue b/frontend/src/views/settings/User.vue index a0da68ce..e461b7c8 100644 --- a/frontend/src/views/settings/User.vue +++ b/frontend/src/views/settings/User.vue @@ -99,6 +99,10 @@ const fetchData = async () => { rules: [], lockPassword: false, id: 0, + quotaLimit: 0, + quotaUnit: "GB", + enforceQuota: false, + quotaUsed: 0, }; } else { const id = Array.isArray(route.params.id) diff --git a/http/http.go b/http/http.go index e6fc185a..6594049b 100644 --- a/http/http.go +++ b/http/http.go @@ -59,6 +59,7 @@ func NewHandler( users.Handle("/{id:[0-9]+}", monkey(userPutHandler, "")).Methods("PUT") users.Handle("/{id:[0-9]+}", monkey(userGetHandler, "")).Methods("GET") users.Handle("/{id:[0-9]+}", monkey(userDeleteHandler, "")).Methods("DELETE") + users.Handle("/{id:[0-9]+}/quota", monkey(userQuotaHandler, "")).Methods("GET") api.PathPrefix("/resources").Handler(monkey(resourceGetHandler, "/api/resources")).Methods("GET") api.PathPrefix("/resources").Handler(monkey(resourceDeleteHandler(fileCache), "/api/resources")).Methods("DELETE") diff --git a/http/quota.go b/http/quota.go new file mode 100644 index 00000000..1f9f5f86 --- /dev/null +++ b/http/quota.go @@ -0,0 +1,65 @@ +package fbhttp + +import ( + "net/http" + "strconv" + + fberrors "github.com/filebrowser/filebrowser/v2/errors" + "github.com/filebrowser/filebrowser/v2/users" +) + +// withQuotaCheck wraps a handler function with quota validation middleware. +// It checks if the user's quota would be exceeded by the operation before allowing it. +func withQuotaCheck(fn handleFunc) handleFunc { + return func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + // Skip quota check if user has no quota limit or quota is not enforced + if d.user.QuotaLimit == 0 || !d.user.EnforceQuota { + return fn(w, r, d) + } + + // Get the file size from the request + fileSize, err := getFileSize(r) + if err != nil { + // If we can't determine file size, allow the operation + // (it will be checked during actual write) + return fn(w, r, d) + } + + // Check if the operation would exceed quota + exceeded, currentUsage, err := users.CheckQuotaExceeded(d.user, fileSize) + if err != nil { + return http.StatusInternalServerError, err + } + + if exceeded { + // Return 413 Payload Too Large with quota information + w.Header().Set("X-Quota-Limit", users.FormatQuotaDisplay(d.user.QuotaLimit)) + w.Header().Set("X-Quota-Used", users.FormatQuotaDisplay(currentUsage)) + return http.StatusRequestEntityTooLarge, fberrors.ErrQuotaExceeded + } + + // Quota check passed, proceed with the operation + return fn(w, r, d) + } +} + +// getFileSize extracts the file size from the HTTP request. +// It checks the Upload-Length header (for TUS uploads) first, then Content-Length. +func getFileSize(r *http.Request) (int64, error) { + // Try to get size from Upload-Length header (TUS protocol) + if uploadLength := r.Header.Get("Upload-Length"); uploadLength != "" { + size, err := strconv.ParseInt(uploadLength, 10, 64) + if err == nil && size > 0 { + return size, nil + } + } + + // Try to get size from Content-Length header + if r.ContentLength > 0 { + return r.ContentLength, nil + } + + // If neither header is available, return 0 + // This might happen with chunked transfer encoding + return 0, nil +} diff --git a/http/resource.go b/http/resource.go index 9fe4caa6..59343f16 100644 --- a/http/resource.go +++ b/http/resource.go @@ -99,7 +99,7 @@ func resourceDeleteHandler(fileCache FileCache) handleFunc { } func resourcePostHandler(fileCache FileCache) handleFunc { - return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + return withUser(withQuotaCheck(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Create || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } @@ -150,10 +150,10 @@ func resourcePostHandler(fileCache FileCache) handleFunc { } return errToStatus(err), err - }) + })) } -var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { +var resourcePutHandler = withUser(withQuotaCheck(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Modify || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } @@ -183,7 +183,7 @@ var resourcePutHandler = withUser(func(w http.ResponseWriter, r *http.Request, d }, "save", r.URL.Path, "", d.user) return errToStatus(err), err -}) +})) func resourcePatchHandler(fileCache FileCache) handleFunc { return withUser(func(_ http.ResponseWriter, r *http.Request, d *data) (int, error) { diff --git a/http/tus_handlers.go b/http/tus_handlers.go index d11920ae..24ee99a3 100644 --- a/http/tus_handlers.go +++ b/http/tus_handlers.go @@ -76,7 +76,7 @@ func keepUploadActive(filePath string) func() { } func tusPostHandler() handleFunc { - return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + return withUser(withQuotaCheck(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Create || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } @@ -155,7 +155,7 @@ func tusPostHandler() handleFunc { w.Header().Set("Location", path) return http.StatusCreated, nil - }) + })) } func tusHeadHandler() handleFunc { @@ -190,7 +190,7 @@ func tusHeadHandler() handleFunc { } func tusPatchHandler() handleFunc { - return withUser(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + return withUser(withQuotaCheck(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { if !d.user.Perm.Create || !d.Check(r.URL.Path) { return http.StatusForbidden, nil } @@ -265,7 +265,7 @@ func tusPatchHandler() handleFunc { } return http.StatusNoContent, nil - }) + })) } func tusDeleteHandler() handleFunc { diff --git a/http/users.go b/http/users.go index 905f123b..d14f2ac6 100644 --- a/http/users.go +++ b/http/users.go @@ -17,7 +17,7 @@ import ( ) var ( - NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules"} + NonModifiableFieldsForNonAdmin = []string{"Username", "Scope", "LockPassword", "Perm", "Commands", "Rules", "QuotaLimit", "QuotaUnit", "EnforceQuota"} ) type modifyUserRequest struct { @@ -208,3 +208,21 @@ var userPutHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request return http.StatusOK, nil }) + +var userQuotaHandler = withSelfOrAdmin(func(w http.ResponseWriter, r *http.Request, d *data) (int, error) { + u, err := d.store.Users.Get(d.server.Root, d.raw.(uint)) + if errors.Is(err, fberrors.ErrNotExist) { + return http.StatusNotFound, err + } + + if err != nil { + return http.StatusInternalServerError, err + } + + quotaInfo, err := users.GetQuotaInfo(u) + if err != nil { + return http.StatusInternalServerError, err + } + + return renderJSON(w, r, quotaInfo) +}) diff --git a/settings/defaults.go b/settings/defaults.go index 5b6c3f2a..8d18a7ca 100644 --- a/settings/defaults.go +++ b/settings/defaults.go @@ -18,6 +18,9 @@ type UserDefaults struct { HideDotfiles bool `json:"hideDotfiles"` DateFormat bool `json:"dateFormat"` AceEditorTheme string `json:"aceEditorTheme"` + QuotaLimit uint64 `json:"quotaLimit"` + QuotaUnit string `json:"quotaUnit"` + EnforceQuota bool `json:"enforceQuota"` } // Apply applies the default options to a user. @@ -32,4 +35,7 @@ func (d *UserDefaults) Apply(u *users.User) { u.HideDotfiles = d.HideDotfiles u.DateFormat = d.DateFormat u.AceEditorTheme = d.AceEditorTheme + u.QuotaLimit = d.QuotaLimit + u.QuotaUnit = d.QuotaUnit + u.EnforceQuota = d.EnforceQuota } diff --git a/users/quota.go b/users/quota.go new file mode 100644 index 00000000..96afd32d --- /dev/null +++ b/users/quota.go @@ -0,0 +1,161 @@ +package users + +import ( + "context" + "fmt" + "os" + "time" + + "github.com/spf13/afero" + + fberrors "github.com/filebrowser/filebrowser/v2/errors" +) + +const ( + // KB represents kilobyte in bytes + KB = 1024 + // MB represents megabyte in bytes + MB = 1024 * KB + // GB represents gigabyte in bytes + GB = 1024 * MB + // TB represents terabyte in bytes + TB = 1024 * GB + // QuotaCalculationTimeout is the maximum time allowed for quota calculation + QuotaCalculationTimeout = 30 * time.Second +) + +// CalculateUserQuota calculates the total disk usage for a user's scope. +// It walks the entire directory tree and sums all file sizes. +func CalculateUserQuota(fs afero.Fs, scope string) (uint64, error) { + ctx, cancel := context.WithTimeout(context.Background(), QuotaCalculationTimeout) + defer cancel() + + var totalSize uint64 + done := make(chan error, 1) + + go func() { + err := afero.Walk(fs, "/", func(path string, info os.FileInfo, err error) error { + if err != nil { + // Skip files/directories that can't be accessed + return nil + } + + // Skip directories, only count files + if !info.IsDir() { + totalSize += uint64(info.Size()) + } + + return nil + }) + done <- err + }() + + select { + case <-ctx.Done(): + return 0, fmt.Errorf("quota calculation timed out after %v", QuotaCalculationTimeout) + case err := <-done: + return totalSize, err + } +} + +// ConvertToBytes converts a value with a unit (KB, MB, GB or TB) to bytes. +func ConvertToBytes(value float64, unit string) (uint64, error) { + switch unit { + case "KB": + return uint64(value * float64(KB)), nil + case "MB": + return uint64(value * float64(MB)), nil + case "GB": + return uint64(value * float64(GB)), nil + case "TB": + return uint64(value * float64(TB)), nil + default: + return 0, fberrors.ErrInvalidQuotaUnit + } +} + +// ConvertFromBytes converts bytes to a value in the specified unit (KB, MB, GB or TB). +func ConvertFromBytes(bytes uint64, unit string) (float64, error) { + switch unit { + case "KB": + return float64(bytes) / float64(KB), nil + case "MB": + return float64(bytes) / float64(MB), nil + case "GB": + return float64(bytes) / float64(GB), nil + case "TB": + return float64(bytes) / float64(TB), nil + default: + return 0, fberrors.ErrInvalidQuotaUnit + } +} + +// FormatQuotaDisplay formats bytes for display, automatically selecting the appropriate unit. +func FormatQuotaDisplay(bytes uint64) string { + if bytes >= TB { + return fmt.Sprintf("%.2f TB", float64(bytes)/float64(TB)) + } + if bytes >= GB { + return fmt.Sprintf("%.2f GB", float64(bytes)/float64(GB)) + } + if bytes >= 1024*1024 { + return fmt.Sprintf("%.2f MB", float64(bytes)/(1024*1024)) + } + if bytes >= 1024 { + return fmt.Sprintf("%.2f KB", float64(bytes)/1024) + } + return fmt.Sprintf("%d B", bytes) +} + +// CheckQuotaExceeded checks if a file operation would exceed the user's quota. +// Returns true if the quota would be exceeded, false otherwise. +func CheckQuotaExceeded(user *User, additionalSize int64) (bool, uint64, error) { + // If quota is not set (0) or not enforced, never exceeded + if user.QuotaLimit == 0 { + return false, 0, nil + } + + // Calculate current usage + currentUsage, err := CalculateUserQuota(user.Fs, user.Scope) + if err != nil { + return false, 0, fmt.Errorf("failed to calculate quota: %w", err) + } + + // Check if adding the new file would exceed the limit + newTotal := currentUsage + uint64(additionalSize) + exceeded := newTotal > user.QuotaLimit + + return exceeded, currentUsage, nil +} + +// GetQuotaInfo returns detailed quota information for a user. +type QuotaInfo struct { + Limit uint64 `json:"limit"` // Quota limit in bytes + Used uint64 `json:"used"` // Current usage in bytes + Unit string `json:"unit"` // Display unit (GB or TB) + Enforce bool `json:"enforce"` // Whether quota is enforced + Percentage float64 `json:"percentage"` // Usage percentage +} + +// GetQuotaInfo retrieves quota information for a user. +func GetQuotaInfo(user *User) (*QuotaInfo, error) { + // Calculate current usage + used, err := CalculateUserQuota(user.Fs, user.Scope) + if err != nil { + return nil, fmt.Errorf("failed to calculate quota usage: %w", err) + } + + // Calculate percentage + var percentage float64 + if user.QuotaLimit > 0 { + percentage = (float64(used) / float64(user.QuotaLimit)) * 100 + } + + return &QuotaInfo{ + Limit: user.QuotaLimit, + Used: used, + Unit: user.QuotaUnit, + Enforce: user.EnforceQuota, + Percentage: percentage, + }, nil +} diff --git a/users/users.go b/users/users.go index 0fcc26d8..3b1c7b18 100644 --- a/users/users.go +++ b/users/users.go @@ -36,6 +36,10 @@ type User struct { HideDotfiles bool `json:"hideDotfiles"` DateFormat bool `json:"dateFormat"` AceEditorTheme string `json:"aceEditorTheme"` + QuotaLimit uint64 `json:"quotaLimit"` // Quota limit in bytes (0 = unlimited) + QuotaUnit string `json:"quotaUnit"` // "KB", "MB", "GB" or "TB" for UI display + EnforceQuota bool `json:"enforceQuota"` // Hard limit if true, soft limit if false + QuotaUsed uint64 `json:"quotaUsed"` // Current usage in bytes (calculated, not stored) } // GetRules implements rules.Provider. @@ -51,6 +55,9 @@ var checkableFields = []string{ "Commands", "Sorting", "Rules", + "QuotaLimit", + "QuotaUnit", + "EnforceQuota", } // Clean cleans up a user and verifies if all its fields @@ -86,6 +93,18 @@ func (u *User) Clean(baseScope string, fields ...string) error { if u.Rules == nil { u.Rules = []rules.Rule{} } + case "QuotaUnit": + if u.QuotaUnit == "" { + u.QuotaUnit = "GB" + } + if u.QuotaUnit != "KB" && u.QuotaUnit != "MB" && u.QuotaUnit != "GB" && u.QuotaUnit != "TB" { + return fberrors.ErrInvalidQuotaUnit + } + case "QuotaLimit": + // QuotaLimit of 0 means unlimited, which is valid + // No validation needed + case "EnforceQuota": + // Boolean field, no validation needed } }