mirror of
https://github.com/photoprism/photoprism.git
synced 2026-01-23 02:24:24 +00:00
UX: Specify files quota in GB instead of MB #4266
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
parent
6eec12c8c9
commit
7a97b38cb3
10 changed files with 59 additions and 29 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue