UX: Specify files quota in GB instead of MB #4266

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2025-03-11 18:04:10 +01:00
parent 6eec12c8c9
commit 7a97b38cb3
10 changed files with 59 additions and 29 deletions

View file

@ -47,7 +47,7 @@ services:
PHOTOPRISM_REGISTER_URI: "https://keycloak.localssl.dev/admin/"
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
PHOTOPRISM_USAGE_INFO: "true"
PHOTOPRISM_FILES_QUOTA: "102400"
PHOTOPRISM_FILES_QUOTA: "100"
## OpenID Connect (pre-configured for local tests):
## see https://keycloak.localssl.dev/realms/master/.well-known/openid-configuration
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/realms/master"

View file

@ -54,7 +54,7 @@ services:
PHOTOPRISM_REGISTER_URI: "https://keycloak.localssl.dev/admin/"
PHOTOPRISM_PASSWORD_RESET_URI: "https://keycloak.localssl.dev/realms/master/login-actions/reset-credentials"
PHOTOPRISM_USAGE_INFO: "true"
PHOTOPRISM_FILES_QUOTA: "102400"
PHOTOPRISM_FILES_QUOTA: "100"
## OpenID Connect (pre-configured for local tests):
## see https://keycloak.localssl.dev/realms/master/.well-known/openid-configuration
PHOTOPRISM_OIDC_URI: "https://keycloak.localssl.dev/realms/master"

View file

@ -755,12 +755,21 @@
</v-list-item>
</v-list>
<div v-if="!isMini && featUsage" class="nav-info usage-info clickable" @click.stop="showUsageInfo">
<div v-if="disconnected" class="nav-info connection-info clickable" @click.stop="showServerConnectionHelp">
<div class="nav-info__underlay"></div>
<div class="text-center">
<v-icon icon="mdi-wifi-off" color="warning" size="21"></v-icon>
</div>
<div v-if="!isMini" class="text-start text-body-2">
{{ $gettext(`No server connection`) }}
</div>
</div>
<div v-else-if="!isMini && featUsage" class="nav-info usage-info clickable" @click.stop="showUsageInfo">
<div class="nav-info__underlay"></div>
<div class="nav-info__content">
<v-progress-linear
:model-value="config.usage.filesUsedPct"
:color="config.usage.filesUsedPct > 95 ? 'error' : 'surface-variant'"
:color="config.usage.filesUsedPct > 95 ? 'error' : 'selected'"
height="16"
max="100"
min="0"
@ -779,15 +788,6 @@
</div>
</div>
<div v-if="disconnected" class="nav-info connection-info clickable" @click.stop="showServerConnectionHelp">
<div class="nav-info__underlay"></div>
<div class="text-center my-1">
<v-icon color="warning" size="25">mdi-wifi-off</v-icon>
</div>
<div v-if="!isMini" class="text-start mt-1 text-body-2">
{{ $gettext(`No server connection`) }}
</div>
</div>
<div v-show="auth && !isPublic && !disconnected" class="nav-info user-info">
<div class="nav-info__underlay"></div>
<div class="nav-user-avatar text-center my-1 mx-2 clickable" @click.stop="showAccountSettings">

View file

@ -106,6 +106,10 @@ func AbortFeatureDisabled(c *gin.Context) {
Abort(c, http.StatusForbidden, i18n.ErrFeatureDisabled)
}
func AbortQuotaExceeded(c *gin.Context) {
Abort(c, http.StatusForbidden, i18n.ErrQuotaExceeded)
}
func AbortBusy(c *gin.Context) {
Abort(c, http.StatusTooManyRequests, i18n.ErrBusy)
}

View file

@ -105,6 +105,11 @@ func (c *ClientConfig) ApplyACL(a acl.ACL, r acl.Role) *ClientConfig {
c.ACL = a.Grants(r)
if !c.ACL[acl.ResourceUsers].Allow(acl.ActionView) {
c.Usage.UsersFreePct = -1
c.Usage.UsersUsedPct = -1
}
return c
}

View file

@ -6,6 +6,7 @@ import (
gc "github.com/patrickmn/go-cache"
"github.com/photoprism/photoprism/internal/entity/query"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/fs/duf"
)
@ -24,6 +25,8 @@ type Usage struct {
FilesFree uint64 `json:"filesFree"`
FilesFreePct int `json:"filesFreePct"`
FilesTotal uint64 `json:"filesTotal"`
UsersUsedPct int `json:"usersUsedPct"`
UsersFreePct int `json:"usersFreePct"`
}
// Usage returns the used, free and total storage size in bytes and caches the result.
@ -70,8 +73,23 @@ func (c *Config) Usage() Usage {
info.FilesUsedPct = 1
}
if info.FilesUsedPct > 100 {
info.FilesUsedPct = 100
}
info.FilesFreePct = 100 - info.FilesUsedPct
if usersTotal := c.UsersQuota(); usersTotal > 0 {
usersUsed := query.CountUsers(true, true, nil, []string{"guest"})
info.UsersUsedPct = int(math.Floor(float64(usersUsed) / float64(usersTotal) * 100))
if info.UsersUsedPct > 100 {
info.UsersUsedPct = 100
}
info.UsersFreePct = 100 - info.UsersUsedPct
}
usageCache.SetDefault(originalsPath, info)
return info
@ -82,7 +100,7 @@ func (c *Config) UsageInfo() bool {
return c.options.UsageInfo || c.options.FilesQuota > 0
}
// FilesQuota returns the maximum aggregated size of all indexed files in megabytes, or 0 if no quota exists.
// FilesQuota returns the maximum aggregated size of all indexed files in gigabytes, or 0 if no limit exists.
func (c *Config) FilesQuota() uint64 {
if c.options.FilesQuota <= 0 {
return 0
@ -91,13 +109,13 @@ func (c *Config) FilesQuota() uint64 {
return c.options.FilesQuota
}
// FilesQuotaBytes returns the maximum aggregated size of all indexed files in bytes, or 0 if no quota exists.
// FilesQuotaBytes returns the maximum aggregated size of all indexed files in bytes, or 0 if no limit exists.
func (c *Config) FilesQuotaBytes() uint64 {
if c.options.FilesQuota <= 0 {
return 0
}
return c.options.FilesQuota * fs.MB
return c.options.FilesQuota * fs.GB
}
// FilesQuotaReached checks if the filesystem usage has been reached or exceeded.

View file

@ -11,13 +11,14 @@ import (
func TestConfig_Usage(t *testing.T) {
c := TestConfig()
FlushUsageCache()
c.options.UsageInfo = true
result := c.Usage()
assert.GreaterOrEqual(t, result.FilesUsed, uint64(60000000))
t.Logf("Storage Used: %d MB (%d%%), Free: %d MB (%d%%), Total %d MB", result.FilesUsed/duf.MB, result.FilesUsedPct, result.FilesFree/duf.MB, result.FilesFreePct, result.FilesTotal/duf.MB)
c.options.FilesQuota = uint64(18)
c.options.FilesQuota = uint64(1)
result2 := c.Usage()
t.Logf("Storage Used: %d MB (%d%%), Free: %d MB (%d%%), Total %d MB", result2.FilesUsed/duf.MB, result2.FilesUsedPct, result2.FilesFree/duf.MB, result2.FilesFreePct, result2.FilesTotal/duf.MB)
@ -27,7 +28,6 @@ func TestConfig_Usage(t *testing.T) {
assert.GreaterOrEqual(t, result2.FilesTotal, uint64(60000000))
FlushUsageCache()
result3 := c.Usage()
t.Logf("Storage Used: %d MB (%d%%), Free: %d MB (%d%%), Total %d MB", result3.FilesUsed/duf.MB, result3.FilesUsedPct, result3.FilesFree/duf.MB, result3.FilesFreePct, result3.FilesTotal/duf.MB)
@ -43,12 +43,13 @@ func TestConfig_Usage(t *testing.T) {
func TestConfig_Quota(t *testing.T) {
c := TestConfig()
FlushUsageCache()
assert.Equal(t, uint64(0), c.FilesQuota())
assert.Equal(t, 0, c.UsersQuota())
c.options.FilesQuota = uint64(18)
c.options.FilesQuota = uint64(1)
c.options.UsersQuota = 10
assert.Equal(t, uint64(18), c.FilesQuota())
assert.Equal(t, uint64(1), c.FilesQuota())
assert.Equal(t, 10, c.UsersQuota())
c.options.FilesQuota = uint64(0)
@ -58,10 +59,16 @@ func TestConfig_Quota(t *testing.T) {
func TestConfig_FilesQuotaReached(t *testing.T) {
c := TestConfig()
FlushUsageCache()
assert.False(t, c.FilesQuotaReached())
c.options.FilesQuota = uint64(18)
c.options.FilesQuota = uint64(1)
FlushUsageCache()
assert.True(t, c.FilesQuotaReached())
c.options.FilesQuota = uint64(5)
FlushUsageCache()
assert.False(t, c.FilesQuotaReached())
c.options.FilesQuota = uint64(0)
}

View file

@ -274,15 +274,9 @@ var Flags = CliFlags{
}}, {
Flag: &cli.Uint64Flag{
Name: "files-quota",
Usage: "maximum aggregated size of all indexed files in `MB` (0 to disable)",
Usage: "maximum aggregated size of all indexed files in `GB` (0 for unlimited)",
EnvVars: EnvVars("FILES_QUOTA"),
}}, {
Flag: &cli.IntFlag{
Name: "users-quota",
Usage: "maximum number of registered user accounts, excluding guests (0 to disable)",
EnvVars: EnvVars("USERS_QUOTA"),
Hidden: true,
}}, {
Flag: &cli.StringFlag{
Name: "backup-path",
Aliases: []string{"ba"},

View file

@ -376,7 +376,7 @@ var FileFixtures = FileMap{
FileRoot: RootOriginals,
OriginalName: "",
FileHash: "acad9168fa6acc5c5c2965ddf6ec465ca42fd831",
FileSize: 7799202,
FileSize: 3 * fs.GB,
FileCodec: "avc1",
FileType: "mp4",
MediaType: media.Video.String(),

View file

@ -48,6 +48,7 @@ const (
ErrAccountConnect
ErrTooManyRequests
ErrStorageIsFull
ErrQuotaExceeded
MsgChangesSaved
MsgAlbumCreated
@ -146,6 +147,7 @@ var Messages = MessageMap{
ErrAccountConnect: gettext("Your account could not be connected"),
ErrTooManyRequests: gettext("Too many requests"),
ErrStorageIsFull: gettext("Storage is full"),
ErrQuotaExceeded: gettext("Quota exceeded"),
// Info and confirmation messages:
MsgChangesSaved: gettext("Changes successfully saved"),