From e58709c822b7934066524bd4a211422af51ecdf5 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Wed, 14 Sep 2022 21:18:32 +0200 Subject: [PATCH] WebAdmin: allow to specify quota and upload size in human format For example 1 GB Signed-off-by: Nicola Murino --- go.mod | 4 +- go.sum | 9 +- internal/httpd/httpd_test.go | 6 +- internal/httpd/webadmin.go | 7 +- internal/util/util.go | 116 ++++++++++++++++++++++- templates/webadmin/group.html | 30 +++--- templates/webadmin/sharedcomponents.html | 6 +- templates/webadmin/user.html | 30 +++--- 8 files changed, 158 insertions(+), 50 deletions(-) diff --git a/go.mod b/go.mod index d399b708..9bd87dbd 100644 --- a/go.mod +++ b/go.mod @@ -70,7 +70,7 @@ require ( golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1 golang.org/x/sys v0.0.0-20220913175220-63ea55921009 golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 - google.golang.org/api v0.95.0 + google.golang.org/api v0.96.0 gopkg.in/natefinch/lumberjack.v2 v2.0.0 ) @@ -156,7 +156,7 @@ require ( golang.org/x/tools v0.1.12 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5 // indirect + google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f // indirect google.golang.org/grpc v1.49.0 // indirect google.golang.org/protobuf v1.28.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index a5f36e9e..03d7b149 100644 --- a/go.sum +++ b/go.sum @@ -976,7 +976,6 @@ golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220513210249-45d2b4557a2a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1123,8 +1122,8 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.95.0 h1:d1c24AAS01DYqXreBeuVV7ewY/U8Mnhh47pwtsgVtYg= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0 h1:F60cuQPJq7K7FzsxMYHAUJSiXh2oKctHxBMbDygxhfM= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1229,8 +1228,8 @@ google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5 h1:ou3VRVAif8UJqz3l1r4Isoz7rrUWHWDHBonShMNYoQs= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f h1:wwbo0UziciPT4Dsca+bmplW53QNAl7tiUOw7FfAcsf8= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index e675b513..8525a1dc 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -16954,7 +16954,7 @@ func TestWebUserAddMock(t *testing.T) { req.Header.Set("Content-Type", contentType) rr = executeRequest(req) checkResponseCode(t, http.StatusOK, rr) - form.Set("max_upload_file_size", "1000") + form.Set("max_upload_file_size", "1 KB") // test invalid default shares expiration form.Set("default_shares_expiration", "a") b, contentType, _ = getMultipartFormData(form, "", "") @@ -17277,7 +17277,7 @@ func TestWebUserUpdateMock(t *testing.T) { assert.NoError(t, err) user.MaxSessions = 1 user.QuotaFiles = 2 - user.QuotaSize = 3 + user.QuotaSize = 1000 * 1000 * 1000 user.GID = 1000 user.Filters.AllowAPIKeyAuth = true user.AdditionalInfo = "new additional info" @@ -17291,7 +17291,7 @@ func TestWebUserUpdateMock(t *testing.T) { form.Set("uid", "0") form.Set("gid", strconv.FormatInt(int64(user.GID), 10)) form.Set("max_sessions", strconv.FormatInt(int64(user.MaxSessions), 10)) - form.Set("quota_size", strconv.FormatInt(user.QuotaSize, 10)) + form.Set("quota_size", "1 GB") form.Set("quota_files", strconv.FormatInt(int64(user.QuotaFiles), 10)) form.Set("upload_bandwidth", "0") form.Set("download_bandwidth", "0") diff --git a/internal/httpd/webadmin.go b/internal/httpd/webadmin.go index 4220d169..8a3f2f0c 100644 --- a/internal/httpd/webadmin.go +++ b/internal/httpd/webadmin.go @@ -482,6 +482,7 @@ func loadAdminTemplates(templatesPath string) { sdk.SFTPFilesystemProvider, sdk.HTTPFilesystemProvider, } }, + "HumanizeBytes": util.ByteCountSI, }) usersTmpl := util.LoadTemplate(nil, usersPaths...) userTmpl := util.LoadTemplate(fsBaseTpl, userPaths...) @@ -1079,7 +1080,7 @@ func getVirtualFoldersFromPostFields(r *http.Request) []vfs.VirtualFolder { QuotaSize: -1, } if len(folderQuotaSizes) > idx { - quotaSize, err := strconv.ParseInt(strings.TrimSpace(folderQuotaSizes[idx]), 10, 64) + quotaSize, err := util.ParseBytes(folderQuotaSizes[idx]) if err == nil { vfolder.QuotaSize = quotaSize } @@ -1305,7 +1306,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error) if err != nil { return filters, err } - maxFileSize, err := strconv.ParseInt(r.Form.Get("max_upload_file_size"), 10, 64) + maxFileSize, err := util.ParseBytes(r.Form.Get("max_upload_file_size")) if err != nil { return filters, fmt.Errorf("invalid max upload file size: %w", err) } @@ -1722,7 +1723,7 @@ func getTransferLimits(r *http.Request) (int64, int64, int64, error) { } func getQuotaLimits(r *http.Request) (int64, int, error) { - quotaSize, err := strconv.ParseInt(r.Form.Get("quota_size"), 10, 64) + quotaSize, err := util.ParseBytes(r.Form.Get("quota_size")) if err != nil { return 0, 0, fmt.Errorf("invalid quota size: %w", err) } diff --git a/internal/util/util.go b/internal/util/util.go index d0e956d6..52931dab 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -29,6 +29,7 @@ import ( "fmt" "io" "io/fs" + "math" "net" "net/http" "net/url" @@ -37,8 +38,10 @@ import ( "path/filepath" "regexp" "runtime" + "strconv" "strings" "time" + "unicode" "github.com/google/uuid" "github.com/lithammer/shortuuid/v3" @@ -59,6 +62,59 @@ var ( additionalSharedDataSearchPath = "" ) +// IEC Sizes. +// kibis of bits +const ( + oneByte = 1 << (iota * 10) + kiByte + miByte + giByte + tiByte + piByte + eiByte +) + +// SI Sizes. +const ( + iByte = 1 + kbByte = iByte * 1000 + mByte = kbByte * 1000 + gByte = mByte * 1000 + tByte = gByte * 1000 + pByte = tByte * 1000 + eByte = pByte * 1000 +) + +var bytesSizeTable = map[string]uint64{ + "b": oneByte, + "kib": kiByte, + "kb": kbByte, + "mib": miByte, + "mb": mByte, + "gib": giByte, + "gb": gByte, + "tib": tiByte, + "tb": tByte, + "pib": piByte, + "pb": pByte, + "eib": eiByte, + "eb": eByte, + // Without suffix + "": oneByte, + "ki": kiByte, + "k": kbByte, + "mi": miByte, + "m": mByte, + "gi": giByte, + "g": gByte, + "ti": tiByte, + "t": tByte, + "pi": piByte, + "p": pByte, + "ei": eiByte, + "e": eByte, +} + // Contains reports whether v is present in elems. func Contains[T comparable](elems []T, v T) bool { for _, s := range elems { @@ -135,6 +191,9 @@ func ByteCountIEC(b int64) string { } func byteCount(b int64, unit int64) string { + if b <= 0 { + return strconv.FormatInt(b, 10) + } if b < unit { return fmt.Sprintf("%d B", b) } @@ -143,12 +202,61 @@ func byteCount(b int64, unit int64) string { div *= unit exp++ } + val := strconv.FormatFloat(float64(b)/float64(div), 'f', -1, 64) if unit == 1000 { - return fmt.Sprintf("%.1f %cB", - float64(b)/float64(div), "KMGTPE"[exp]) + return fmt.Sprintf("%s %cB", val, "KMGTPE"[exp]) } - return fmt.Sprintf("%.1f %ciB", - float64(b)/float64(div), "KMGTPE"[exp]) + return fmt.Sprintf("%s %ciB", val, "KMGTPE"[exp]) +} + +// ParseBytes parses a string representation of bytes into the number +// of bytes it represents. +// +// ParseBytes("42 MB") -> 42000000, nil +// ParseBytes("42 mib") -> 44040192, nil +// +// copied from here: +// +// https://github.com/dustin/go-humanize/blob/master/bytes.go +// +// with minor modifications +func ParseBytes(s string) (int64, error) { + s = strings.TrimSpace(s) + lastDigit := 0 + hasComma := false + for _, r := range s { + if !(unicode.IsDigit(r) || r == '.' || r == ',') { + break + } + if r == ',' { + hasComma = true + } + lastDigit++ + } + + num := s[:lastDigit] + if hasComma { + num = strings.Replace(num, ",", "", -1) + } + + f, err := strconv.ParseFloat(num, 64) + if err != nil { + return 0, err + } + + extra := strings.ToLower(strings.TrimSpace(s[lastDigit:])) + if m, ok := bytesSizeTable[extra]; ok { + f *= float64(m) + if f >= math.MaxInt64 { + return 0, fmt.Errorf("value too large: %v", s) + } + if f < 0 { + return 0, fmt.Errorf("negative value not allowed: %v", s) + } + return int64(f), nil + } + + return 0, fmt.Errorf("unhandled size name: %v", extra) } // GetIPFromRemoteAddress returns the IP from the remote address. diff --git a/templates/webadmin/group.html b/templates/webadmin/group.html index 3f604f27..349c8a0f 100644 --- a/templates/webadmin/group.html +++ b/templates/webadmin/group.html @@ -64,7 +64,7 @@ along with this program. If not, see . Virtual folders
-
Quota -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders
+
Quota size -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders. You can use MB/GB/TB suffix. With no suffix we assume bytes
{{range $idx, $val := .Group.VirtualFolders}} @@ -81,10 +81,10 @@ along with this program. If not, see .
- + - Quota size (bytes) + Quota size
@@ -114,10 +114,10 @@ along with this program. If not, see .
- + - Quota size (bytes) + Quota size
@@ -386,12 +386,12 @@ along with this program. If not, see .
- +
- + - 0 means no limit + 0 means no limit. You can use MB/GB/TB suffix
@@ -406,13 +406,13 @@ along with this program. If not, see .
- +
- - Maximum upload size for a single file. 0 means no limit + Maximum upload size for a single file. 0 means no limit. You can use MB/GB/TB suffix
diff --git a/templates/webadmin/sharedcomponents.html b/templates/webadmin/sharedcomponents.html index 51bee09b..f1c9cb5c 100644 --- a/templates/webadmin/sharedcomponents.html +++ b/templates/webadmin/sharedcomponents.html @@ -63,10 +63,10 @@ along with this program. If not, see .
- + - Quota size (bytes) + Quota size
diff --git a/templates/webadmin/user.html b/templates/webadmin/user.html index 604014f8..39f616a3 100644 --- a/templates/webadmin/user.html +++ b/templates/webadmin/user.html @@ -203,7 +203,7 @@ along with this program. If not, see . Virtual folders
-
Quota -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders
+
Quota size -1 means included within user quota, 0 unlimited. Don't set -1 for shared folders. You can use MB/GB/TB suffix. With no suffix we assume bytes
{{range $idx, $val := .User.VirtualFolders}} @@ -220,10 +220,10 @@ along with this program. If not, see .
- + - Quota size (bytes) + Quota size
@@ -253,10 +253,10 @@ along with this program. If not, see .
- + - Quota size (bytes) + Quota size
@@ -603,12 +603,12 @@ along with this program. If not, see .
- +
- + - 0 means no limit + 0 means no limit. You can use MB/GB/TB suffix
@@ -623,13 +623,13 @@ along with this program. If not, see .
- +
- - Maximum upload size for a single file. 0 means no limit + Maximum upload size for a single file. 0 means no limit. You can use MB/GB/TB suffix