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