mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-09 10:40:33 +08:00
Config: Add option to show filesystem usage in sidebar navigation #4266
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -44,6 +44,38 @@ let start = new Date();
|
|||||||
const debug = window.__CONFIG__?.debug || window.__CONFIG__?.trace;
|
const debug = window.__CONFIG__?.debug || window.__CONFIG__?.trace;
|
||||||
|
|
||||||
export default class $util {
|
export default class $util {
|
||||||
|
static formatBytes(b) {
|
||||||
|
if (!b) {
|
||||||
|
return "0 KB";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof b === "string") {
|
||||||
|
b = Number.parseFloat(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (b >= 1073741824) {
|
||||||
|
const gb = b / 1073741824;
|
||||||
|
return gb.toFixed(1) + " GB";
|
||||||
|
} else if (b >= 1048576) {
|
||||||
|
const mb = b / 1048576;
|
||||||
|
return mb.toFixed(1) + " MB";
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.ceil(b / 1024) + " KB";
|
||||||
|
}
|
||||||
|
|
||||||
|
static gigaBytes(b) {
|
||||||
|
if (!b) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof b === "string") {
|
||||||
|
b = Number.parseFloat(b);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(b / 1073741824);
|
||||||
|
}
|
||||||
|
|
||||||
static formatDate(s) {
|
static formatDate(s) {
|
||||||
if (!s || !s.length) {
|
if (!s || !s.length) {
|
||||||
return s;
|
return s;
|
||||||
|
@@ -137,6 +137,13 @@ export default {
|
|||||||
flat: true,
|
flat: true,
|
||||||
transition: false,
|
transition: false,
|
||||||
},
|
},
|
||||||
|
VNavigationDrawer: {
|
||||||
|
width: 270,
|
||||||
|
railWidth: 70,
|
||||||
|
mobileBreakpoint: 960,
|
||||||
|
location: "start",
|
||||||
|
touchless: true,
|
||||||
|
},
|
||||||
VListItem: {
|
VListItem: {
|
||||||
ripple: false,
|
ripple: false,
|
||||||
},
|
},
|
||||||
|
@@ -50,14 +50,9 @@
|
|||||||
<v-navigation-drawer
|
<v-navigation-drawer
|
||||||
v-if="auth"
|
v-if="auth"
|
||||||
v-model="drawer"
|
v-model="drawer"
|
||||||
color="navigation"
|
|
||||||
:rail="isMini"
|
:rail="isMini"
|
||||||
:rail-width="70"
|
color="navigation"
|
||||||
:width="270"
|
|
||||||
:mobile-breakpoint="960"
|
|
||||||
class="nav-sidebar navigation"
|
class="nav-sidebar navigation"
|
||||||
location="start"
|
|
||||||
touchless
|
|
||||||
>
|
>
|
||||||
<div class="nav-container">
|
<div class="nav-container">
|
||||||
<v-toolbar flat :density="$vuetify.display.smAndDown ? 'compact' : 'default'">
|
<v-toolbar flat :density="$vuetify.display.smAndDown ? 'compact' : 'default'">
|
||||||
@@ -760,6 +755,34 @@
|
|||||||
</v-list-item>
|
</v-list-item>
|
||||||
</v-list>
|
</v-list>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-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
|
||||||
|
:color="config.usage.usedPct > 95 ? 'error' : 'surface-variant'"
|
||||||
|
height="16"
|
||||||
|
max="100"
|
||||||
|
min="0"
|
||||||
|
width="100%"
|
||||||
|
:model-value="config.usage.usedPct"
|
||||||
|
rounded
|
||||||
|
>
|
||||||
|
<div class="text-caption opacity-85">
|
||||||
|
{{
|
||||||
|
$gettext(`%{n} GB of %{q} GB used`, {
|
||||||
|
n: $util.gigaBytes(config.usage.used),
|
||||||
|
q: $util.gigaBytes(config.usage.total),
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</v-progress-linear>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-if="disconnected" class="nav-info connection-info clickable" @click.stop="showServerConnectionHelp">
|
<div v-if="disconnected" class="nav-info connection-info clickable" @click.stop="showServerConnectionHelp">
|
||||||
<div class="nav-info__underlay"></div>
|
<div class="nav-info__underlay"></div>
|
||||||
<div class="text-center my-1">
|
<div class="text-center my-1">
|
||||||
@@ -935,6 +958,7 @@ export default {
|
|||||||
appNameSuffix = appNameParts.slice(1, 9).join(" ");
|
appNameSuffix = appNameParts.slice(1, 9).join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canManagePhotos = this.$config.allow("photos", "manage");
|
||||||
const isDemo = this.$config.get("demo");
|
const isDemo = this.$config.get("demo");
|
||||||
const isPro = !!this.$config.values?.ext["pro"];
|
const isPro = !!this.$config.values?.ext["pro"];
|
||||||
const isPublic = this.$config.get("public");
|
const isPublic = this.$config.get("public");
|
||||||
@@ -946,7 +970,7 @@ export default {
|
|||||||
return {
|
return {
|
||||||
canSearchPlaces: this.$config.allow("places", "search"),
|
canSearchPlaces: this.$config.allow("places", "search"),
|
||||||
canAccessPrivate: !isRestricted && this.$config.allow("photos", "access_private"),
|
canAccessPrivate: !isRestricted && this.$config.allow("photos", "access_private"),
|
||||||
canManagePhotos: this.$config.allow("photos", "manage"),
|
canManagePhotos: canManagePhotos,
|
||||||
canManagePeople: this.$config.allow("people", "manage"),
|
canManagePeople: this.$config.allow("people", "manage"),
|
||||||
canManageUsers: (!isPublic || isDemo) && this.$config.allow("users", "manage"),
|
canManageUsers: (!isPublic || isDemo) && this.$config.allow("users", "manage"),
|
||||||
appNameSuffix: appNameSuffix,
|
appNameSuffix: appNameSuffix,
|
||||||
@@ -960,6 +984,8 @@ export default {
|
|||||||
featUpgrade: tier < 6 && isSuperAdmin && !isPublic && !isDemo,
|
featUpgrade: tier < 6 && isSuperAdmin && !isPublic && !isDemo,
|
||||||
featMembership: tier < 3 && isSuperAdmin && !isPublic && !isDemo,
|
featMembership: tier < 3 && isSuperAdmin && !isPublic && !isDemo,
|
||||||
featFeedback: tier >= 6 && isSuperAdmin && !isPublic && !isDemo,
|
featFeedback: tier >= 6 && isSuperAdmin && !isPublic && !isDemo,
|
||||||
|
featFiles: this.$config.feature("files"),
|
||||||
|
featUsage: !isDemo && canManagePhotos && this.$config.feature("files") && this.$config.values?.usage?.total,
|
||||||
isRestricted: isRestricted,
|
isRestricted: isRestricted,
|
||||||
isMini: localStorage.getItem("last_navigation_mode") !== "false" || isRestricted,
|
isMini: localStorage.getItem("last_navigation_mode") !== "false" || isRestricted,
|
||||||
isDemo: isDemo,
|
isDemo: isDemo,
|
||||||
@@ -1070,14 +1096,17 @@ export default {
|
|||||||
this.isMini = !this.isMini;
|
this.isMini = !this.isMini;
|
||||||
localStorage.setItem("last_navigation_mode", `${this.isMini}`);
|
localStorage.setItem("last_navigation_mode", `${this.isMini}`);
|
||||||
},
|
},
|
||||||
showAccountSettings: function () {
|
showAccountSettings() {
|
||||||
if (this.$config.feature("account")) {
|
if (this.$config.feature("account")) {
|
||||||
this.$router.push({ name: "settings_account" });
|
this.$router.push({ name: "settings_account" });
|
||||||
} else {
|
} else {
|
||||||
this.$router.push({ name: "settings" });
|
this.$router.push({ name: "settings" });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showServerConnectionHelp: function () {
|
showUsageInfo() {
|
||||||
|
this.$router.push({ path: "/index/files" });
|
||||||
|
},
|
||||||
|
showServerConnectionHelp() {
|
||||||
this.$router.push({ path: "/help/websockets" });
|
this.$router.push({ path: "/help/websockets" });
|
||||||
},
|
},
|
||||||
showLegalInfo() {
|
showLegalInfo() {
|
||||||
|
@@ -176,7 +176,9 @@
|
|||||||
<td>
|
<td>
|
||||||
{{ $gettext(`Size`) }}
|
{{ $gettext(`Size`) }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ file.sizeInfo() }}</td>
|
<td>
|
||||||
|
<span v-tooltip="Math.ceil(file?.Size / 1024).toLocaleString() + ' KB'">{{ file.sizeInfo() }}</span>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="file.Software">
|
<tr v-if="file.Software">
|
||||||
<td>
|
<td>
|
||||||
|
@@ -348,6 +348,13 @@ nav .v-list__item__title.title {
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.nav-sidebar .nav-container > .nav-info > .nav-info__content {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px 8px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.nav-sidebar .nav-container > .nav-info:hover > .nav-info__underlay {
|
.nav-sidebar .nav-container > .nav-info:hover > .nav-info__underlay {
|
||||||
opacity: var(--v-hover-opacity, .018);
|
opacity: var(--v-hover-opacity, .018);
|
||||||
}
|
}
|
||||||
|
@@ -252,14 +252,8 @@ export class File extends RestModel {
|
|||||||
info.push(this.Width + " × " + this.Height);
|
info.push(this.Width + " × " + this.Height);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.Size > 102400) {
|
if (this.Size) {
|
||||||
const size = Number.parseFloat(this.Size) / 1048576;
|
info.push($util.formatBytes(this.Size));
|
||||||
|
|
||||||
info.push(size.toFixed(1) + " MB");
|
|
||||||
} else if (this.Size) {
|
|
||||||
const size = Number.parseFloat(this.Size) / 1024;
|
|
||||||
|
|
||||||
info.push(size.toFixed(1) + " KB");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -875,13 +875,7 @@ export class Photo extends RestModel {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.Size > 102400) {
|
info.push($util.formatBytes(file.Size));
|
||||||
const size = Number.parseFloat(file.Size) / 1048576;
|
|
||||||
info.push(size.toFixed(1) + " MB");
|
|
||||||
} else {
|
|
||||||
const size = Number.parseFloat(file.Size) / 1024;
|
|
||||||
info.push(size.toFixed(1) + " KB");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
vectorFile() {
|
vectorFile() {
|
||||||
|
@@ -7,6 +7,18 @@ let chai = require("chai/chai");
|
|||||||
let assert = chai.assert;
|
let assert = chai.assert;
|
||||||
|
|
||||||
describe("common/util", () => {
|
describe("common/util", () => {
|
||||||
|
it("should return size in KB", () => {
|
||||||
|
const s = $util.formatBytes(10 * 1024);
|
||||||
|
assert.equal(s, "10 KB");
|
||||||
|
});
|
||||||
|
it("should return size in GB", () => {
|
||||||
|
const s = $util.formatBytes(10 * 1024 * 1024 * 1024);
|
||||||
|
assert.equal(s, "10.0 GB");
|
||||||
|
});
|
||||||
|
it("should convert bytes in GB", () => {
|
||||||
|
const b = $util.gigaBytes(10 * 1024 * 1024 * 1024);
|
||||||
|
assert.equal(b, 10);
|
||||||
|
});
|
||||||
it("should return duration 3ns", () => {
|
it("should return duration 3ns", () => {
|
||||||
const duration = $util.formatDuration(-3);
|
const duration = $util.formatDuration(-3);
|
||||||
assert.equal(duration, "3ns");
|
assert.equal(duration, "3ns");
|
||||||
|
@@ -347,7 +347,7 @@ describe("model/file", () => {
|
|||||||
UpdatedAt: "2012-07-08T14:45:39Z",
|
UpdatedAt: "2012-07-08T14:45:39Z",
|
||||||
};
|
};
|
||||||
const file = new File(values);
|
const file = new File(values);
|
||||||
assert.equal(file.sizeInfo(), "7.8 KB");
|
assert.equal(file.sizeInfo(), "8 KB");
|
||||||
const values2 = {
|
const values2 = {
|
||||||
InstanceID: 5,
|
InstanceID: 5,
|
||||||
UID: "ABC123",
|
UID: "ABC123",
|
||||||
@@ -359,7 +359,7 @@ describe("model/file", () => {
|
|||||||
UpdatedAt: "2012-07-08T14:45:39Z",
|
UpdatedAt: "2012-07-08T14:45:39Z",
|
||||||
};
|
};
|
||||||
const file2 = new File(values2);
|
const file2 = new File(values2);
|
||||||
assert.equal(file2.sizeInfo(), "7638.9 MB");
|
assert.equal(file2.sizeInfo(), "7.5 GB");
|
||||||
const values3 = {
|
const values3 = {
|
||||||
InstanceID: 5,
|
InstanceID: 5,
|
||||||
UID: "ABC123",
|
UID: "ABC123",
|
||||||
@@ -373,7 +373,7 @@ describe("model/file", () => {
|
|||||||
UpdatedAt: "2012-07-08T14:45:39Z",
|
UpdatedAt: "2012-07-08T14:45:39Z",
|
||||||
};
|
};
|
||||||
const file3 = new File(values3);
|
const file3 = new File(values3);
|
||||||
assert.equal(file3.sizeInfo(), "500 × 800, 7638.9 MB");
|
assert.equal(file3.sizeInfo(), "500 × 800, 7.5 GB");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should like file", () => {
|
it("should like file", () => {
|
||||||
|
@@ -1095,7 +1095,7 @@ describe("model/photo", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const photo3 = new Photo(values3);
|
const photo3 = new Photo(values3);
|
||||||
assert.equal(photo3.getVideoInfo(), "6µs, AVC, 500 × 600, 0.2 MB");
|
assert.equal(photo3.getVideoInfo(), "6µs, AVC, 500 × 600, 218 KB");
|
||||||
const values4 = {
|
const values4 = {
|
||||||
ID: 10,
|
ID: 10,
|
||||||
UID: "ABC127",
|
UID: "ABC127",
|
||||||
@@ -1122,7 +1122,7 @@ describe("model/photo", () => {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
const photo4 = new Photo(values4);
|
const photo4 = new Photo(values4);
|
||||||
assert.equal(photo4.getVideoInfo(), "6µs, AVC, 300 × 500, 10.0 KB");
|
assert.equal(photo4.getVideoInfo(), "6µs, AVC, 300 × 500, 10 KB");
|
||||||
assert.equal(photo4.getDurationInfo(), "6µs");
|
assert.equal(photo4.getDurationInfo(), "6µs");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -91,6 +91,7 @@ type ClientConfig struct {
|
|||||||
Categories CategoryLabels `json:"categories"`
|
Categories CategoryLabels `json:"categories"`
|
||||||
Clip int `json:"clip"`
|
Clip int `json:"clip"`
|
||||||
Server env.Resources `json:"server"`
|
Server env.Resources `json:"server"`
|
||||||
|
Usage Usage `json:"usage"`
|
||||||
Settings *customize.Settings `json:"settings,omitempty"`
|
Settings *customize.Settings `json:"settings,omitempty"`
|
||||||
ACL acl.Grants `json:"acl,omitempty"`
|
ACL acl.Grants `json:"acl,omitempty"`
|
||||||
Ext Map `json:"ext"`
|
Ext Map `json:"ext"`
|
||||||
@@ -510,6 +511,7 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
|
|||||||
ManifestUri: c.ClientManifestUri(),
|
ManifestUri: c.ClientManifestUri(),
|
||||||
Clip: txt.ClipDefault,
|
Clip: txt.ClipDefault,
|
||||||
Server: env.Info(),
|
Server: env.Info(),
|
||||||
|
Usage: c.Usage(),
|
||||||
Ext: ClientExt(c, ClientUser),
|
Ext: ClientExt(c, ClientUser),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -663,8 +663,8 @@ func (c *Config) OriginalsLimit() int {
|
|||||||
return c.options.OriginalsLimit
|
return c.options.OriginalsLimit
|
||||||
}
|
}
|
||||||
|
|
||||||
// OriginalsByteLimit returns the maximum size of originals in bytes.
|
// OriginalsLimitBytes returns the maximum size of originals in bytes.
|
||||||
func (c *Config) OriginalsByteLimit() int64 {
|
func (c *Config) OriginalsLimitBytes() int64 {
|
||||||
if result := c.OriginalsLimit(); result <= 0 {
|
if result := c.OriginalsLimit(); result <= 0 {
|
||||||
return -1
|
return -1
|
||||||
} else {
|
} else {
|
||||||
|
@@ -339,12 +339,12 @@ func TestConfig_OriginalsLimit(t *testing.T) {
|
|||||||
assert.Equal(t, 800, c.OriginalsLimit())
|
assert.Equal(t, 800, c.OriginalsLimit())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_OriginalsByteLimit(t *testing.T) {
|
func TestConfig_OriginalsLimitBytes(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
assert.Equal(t, int64(-1), c.OriginalsByteLimit())
|
assert.Equal(t, int64(-1), c.OriginalsLimitBytes())
|
||||||
c.options.OriginalsLimit = 800
|
c.options.OriginalsLimit = 800
|
||||||
assert.Equal(t, int64(838860800), c.OriginalsByteLimit())
|
assert.Equal(t, int64(838860800), c.OriginalsLimitBytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_ResolutionLimit(t *testing.T) {
|
func TestConfig_ResolutionLimit(t *testing.T) {
|
||||||
|
110
internal/config/config_usage.go
Normal file
110
internal/config/config_usage.go
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
gc "github.com/patrickmn/go-cache"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs/duf"
|
||||||
|
)
|
||||||
|
|
||||||
|
var usageInfoCache = gc.New(5*time.Minute, 5*time.Minute)
|
||||||
|
|
||||||
|
// FlushUsageInfoCache resets the usage information cache.
|
||||||
|
func FlushUsageInfoCache() {
|
||||||
|
usageInfoCache.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage represents storage usage information.
|
||||||
|
type Usage struct {
|
||||||
|
Used uint64 `json:"used"`
|
||||||
|
UsedPct int `json:"usedPct"`
|
||||||
|
Free uint64 `json:"free"`
|
||||||
|
FreePct int `json:"freePct"`
|
||||||
|
Total uint64 `json:"total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage returns the used, free and total storage size in bytes and caches the result.
|
||||||
|
func (c *Config) Usage() Usage {
|
||||||
|
// Return nil if feature is not enabled.
|
||||||
|
if !c.UsageInfo() {
|
||||||
|
return Usage{}
|
||||||
|
}
|
||||||
|
|
||||||
|
originalsPath := c.OriginalsPath()
|
||||||
|
|
||||||
|
if cached, hit := usageInfoCache.Get(originalsPath); hit && cached != nil {
|
||||||
|
return cached.(Usage)
|
||||||
|
}
|
||||||
|
|
||||||
|
info := Usage{}
|
||||||
|
|
||||||
|
if err := c.Db().Unscoped().
|
||||||
|
Table("files").
|
||||||
|
Select("SUM(file_size) AS used").
|
||||||
|
Where("deleted_at IS NULL").
|
||||||
|
Take(&info).Error; err != nil {
|
||||||
|
log.Warnf("config: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
quotaTotal := c.QuotaBytes()
|
||||||
|
|
||||||
|
if m, err := duf.PathInfo(originalsPath); err == nil {
|
||||||
|
info.Free = m.Free
|
||||||
|
info.Total = info.Used + m.Free
|
||||||
|
|
||||||
|
if quotaTotal > 0 && (info.Total <= 0 || quotaTotal < info.Total) {
|
||||||
|
info.Total = quotaTotal
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Total > 0 {
|
||||||
|
info.UsedPct = int(math.RoundToEven(float64(info.Used) / float64(info.Total) * 100))
|
||||||
|
}
|
||||||
|
} else if quotaTotal > 0 {
|
||||||
|
info.Total = quotaTotal
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.Used > 0 && info.UsedPct <= 0 {
|
||||||
|
info.UsedPct = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
info.FreePct = 100 - info.UsedPct
|
||||||
|
|
||||||
|
usageInfoCache.SetDefault(originalsPath, info)
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
// UsageInfo returns true if resource usage information should be displayed in the user interface.
|
||||||
|
func (c *Config) UsageInfo() bool {
|
||||||
|
return c.options.UsageInfo || c.options.Quota > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Quota returns the maximum aggregated size of all indexed files in megabytes, or 0 if no quota exists.
|
||||||
|
func (c *Config) Quota() uint64 {
|
||||||
|
if c.options.Quota <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.Quota
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaBytes returns the maximum aggregated size of all indexed files in bytes, or 0 if no quota exists.
|
||||||
|
func (c *Config) QuotaBytes() uint64 {
|
||||||
|
if c.options.Quota <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.Quota * fs.MB
|
||||||
|
}
|
||||||
|
|
||||||
|
// QuotaUsers returns the maximum number of registered user accounts, or 0 if no quota exists.
|
||||||
|
func (c *Config) QuotaUsers() int {
|
||||||
|
if c.options.QuotaUsers <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.options.QuotaUsers
|
||||||
|
}
|
28
internal/config/config_usage_test.go
Normal file
28
internal/config/config_usage_test.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs/duf"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig_Usage(t *testing.T) {
|
||||||
|
c := TestConfig()
|
||||||
|
|
||||||
|
c.options.UsageInfo = true
|
||||||
|
result := c.Usage()
|
||||||
|
assert.GreaterOrEqual(t, result.Used, uint64(60000000))
|
||||||
|
t.Logf("Storage Used: %d MB (%d%%), Free: %d MB (%d%%), Total %d MB", result.Used/duf.MB, result.UsedPct, result.Free/duf.MB, result.FreePct, result.Total/duf.MB)
|
||||||
|
|
||||||
|
c.options.UsageInfo = false
|
||||||
|
assert.Equal(t, c.Usage().Used, uint64(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_Quota(t *testing.T) {
|
||||||
|
c := TestConfig()
|
||||||
|
|
||||||
|
assert.Equal(t, uint64(0), c.Quota())
|
||||||
|
assert.Equal(t, 0, c.QuotaUsers())
|
||||||
|
}
|
@@ -267,6 +267,23 @@ var Flags = CliFlags{
|
|||||||
Usage: "create YAML sidecar files to back up picture metadata",
|
Usage: "create YAML sidecar files to back up picture metadata",
|
||||||
EnvVars: EnvVars("SIDECAR_YAML"),
|
EnvVars: EnvVars("SIDECAR_YAML"),
|
||||||
}, DocDefault: "true"}, {
|
}, DocDefault: "true"}, {
|
||||||
|
Flag: &cli.BoolFlag{
|
||||||
|
Name: "usage-info",
|
||||||
|
Usage: "display usage information in the user interface",
|
||||||
|
EnvVars: EnvVars("USAGE_INFO"),
|
||||||
|
}}, {
|
||||||
|
Flag: &cli.Uint64Flag{
|
||||||
|
Name: "quota",
|
||||||
|
Usage: "maximum aggregated size of all indexed files in `MB` (0 to disable)",
|
||||||
|
EnvVars: EnvVars("QUOTA"),
|
||||||
|
Hidden: true,
|
||||||
|
}}, {
|
||||||
|
Flag: &cli.IntFlag{
|
||||||
|
Name: "quota-users",
|
||||||
|
Usage: "maximum number of registered user accounts, excluding guests (0 to disable)",
|
||||||
|
EnvVars: EnvVars("QUOTA_USERS"),
|
||||||
|
Hidden: true,
|
||||||
|
}}, {
|
||||||
Flag: &cli.StringFlag{
|
Flag: &cli.StringFlag{
|
||||||
Name: "backup-path",
|
Name: "backup-path",
|
||||||
Aliases: []string{"ba"},
|
Aliases: []string{"ba"},
|
||||||
|
@@ -26,7 +26,7 @@ type Options struct {
|
|||||||
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
|
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
|
||||||
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
|
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
|
||||||
Public bool `yaml:"Public" json:"-" flag:"public"`
|
Public bool `yaml:"Public" json:"-" flag:"public"`
|
||||||
NoHub bool `yaml:"NoHub" json:"-" flag:"no-hub"`
|
NoHub bool `yaml:"-" json:"-" flag:"no-hub"`
|
||||||
AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"`
|
AdminUser string `yaml:"AdminUser" json:"-" flag:"admin-user"`
|
||||||
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
|
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
|
||||||
PasswordLength int `yaml:"PasswordLength" json:"-" flag:"password-length"`
|
PasswordLength int `yaml:"PasswordLength" json:"-" flag:"password-length"`
|
||||||
@@ -72,6 +72,9 @@ type Options struct {
|
|||||||
CustomAssetsPath string `yaml:"-" json:"-" flag:"custom-assets-path"`
|
CustomAssetsPath string `yaml:"-" json:"-" flag:"custom-assets-path"`
|
||||||
SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"`
|
SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"`
|
||||||
SidecarYaml bool `yaml:"SidecarYaml" json:"SidecarYaml" flag:"sidecar-yaml" default:"true"`
|
SidecarYaml bool `yaml:"SidecarYaml" json:"SidecarYaml" flag:"sidecar-yaml" default:"true"`
|
||||||
|
UsageInfo bool `yaml:"UsageInfo" json:"UsageInfo" flag:"usage-info"`
|
||||||
|
Quota uint64 `yaml:"Quota" json:"Quota" flag:"quota"`
|
||||||
|
QuotaUsers int `yaml:"QuotaUsers" json:"QuotaUsers" flag:"quota-users"`
|
||||||
BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"`
|
BackupPath string `yaml:"BackupPath" json:"-" flag:"backup-path"`
|
||||||
BackupSchedule string `yaml:"BackupSchedule" json:"BackupSchedule" flag:"backup-schedule"`
|
BackupSchedule string `yaml:"BackupSchedule" json:"BackupSchedule" flag:"backup-schedule"`
|
||||||
BackupRetain int `yaml:"BackupRetain" json:"BackupRetain" flag:"backup-retain"`
|
BackupRetain int `yaml:"BackupRetain" json:"BackupRetain" flag:"backup-retain"`
|
||||||
|
@@ -79,6 +79,11 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||||||
{"sidecar-path", c.SidecarPath()},
|
{"sidecar-path", c.SidecarPath()},
|
||||||
{"sidecar-yaml", fmt.Sprintf("%t", c.SidecarYaml())},
|
{"sidecar-yaml", fmt.Sprintf("%t", c.SidecarYaml())},
|
||||||
|
|
||||||
|
// Usage.
|
||||||
|
{"usage-info", fmt.Sprintf("%t", c.UsageInfo())},
|
||||||
|
{"quota", fmt.Sprintf("%d", c.Quota())},
|
||||||
|
{"quota-users", fmt.Sprintf("%d", c.QuotaUsers())},
|
||||||
|
|
||||||
// Backups.
|
// Backups.
|
||||||
{"backup-path", c.BackupBasePath()},
|
{"backup-path", c.BackupBasePath()},
|
||||||
{"backup-schedule", c.BackupSchedule()},
|
{"backup-schedule", c.BackupSchedule()},
|
||||||
|
@@ -19,6 +19,7 @@ var OptionsReportSections = []ReportSection{
|
|||||||
{Start: "PHOTOPRISM_LOG_LEVEL", Title: "Logging"},
|
{Start: "PHOTOPRISM_LOG_LEVEL", Title: "Logging"},
|
||||||
{Start: "PHOTOPRISM_CONFIG_PATH", Title: "Storage"},
|
{Start: "PHOTOPRISM_CONFIG_PATH", Title: "Storage"},
|
||||||
{Start: "PHOTOPRISM_SIDECAR_PATH", Title: "Sidecar Files"},
|
{Start: "PHOTOPRISM_SIDECAR_PATH", Title: "Sidecar Files"},
|
||||||
|
{Start: "PHOTOPRISM_USAGE_INFO", Title: "Usage"},
|
||||||
{Start: "PHOTOPRISM_BACKUP_PATH", Title: "Backup"},
|
{Start: "PHOTOPRISM_BACKUP_PATH", Title: "Backup"},
|
||||||
{Start: "PHOTOPRISM_INDEX_WORKERS, PHOTOPRISM_WORKERS", Title: "Indexing"},
|
{Start: "PHOTOPRISM_INDEX_WORKERS, PHOTOPRISM_WORKERS", Title: "Indexing"},
|
||||||
{Start: "PHOTOPRISM_READONLY", Title: "Feature Flags"},
|
{Start: "PHOTOPRISM_READONLY", Title: "Feature Flags"},
|
||||||
|
@@ -25,7 +25,7 @@ func CachedAlbumByUID(uid string) (m Album, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cached?
|
// Cached?
|
||||||
if cacheData, ok := albumCache.Get(uid); ok {
|
if cacheData, hit := albumCache.Get(uid); hit {
|
||||||
log.Tracef("album: cache hit for %s", uid)
|
log.Tracef("album: cache hit for %s", uid)
|
||||||
return cacheData.(Album), nil
|
return cacheData.(Album), nil
|
||||||
}
|
}
|
||||||
|
@@ -254,7 +254,7 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Remove(file); err != nil {
|
if err = os.Remove(file); err != nil {
|
||||||
log.Errorf("import: failed removing %s (%s)", clean.Log(fs.RelName(file, importPath)), err.Error())
|
log.Errorf("import: failed removing %s (%s)", clean.Log(fs.RelName(file, importPath)), err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -268,16 +268,17 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
|||||||
// Run face recognition if enabled.
|
// Run face recognition if enabled.
|
||||||
if w := NewFaces(imp.conf); w.Disabled() {
|
if w := NewFaces(imp.conf); w.Disabled() {
|
||||||
log.Debugf("import: skipping face recognition")
|
log.Debugf("import: skipping face recognition")
|
||||||
} else if err := w.Start(FacesOptionsDefault()); err != nil {
|
} else if err = w.Start(FacesOptionsDefault()); err != nil {
|
||||||
log.Errorf("import: %s", err)
|
log.Errorf("import: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update photo counts and visibilities.
|
// Update photo counts and visibilities.
|
||||||
if err := entity.UpdateCounts(); err != nil {
|
if err = entity.UpdateCounts(); err != nil {
|
||||||
log.Warnf("index: %s (update counts)", err)
|
log.Warnf("index: %s (update counts)", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.FlushUsageInfoCache()
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
|
|
||||||
return done
|
return done
|
||||||
|
@@ -321,13 +321,14 @@ func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Update precalculated photo and file counts.
|
// Update precalculated photo and file counts.
|
||||||
if err := entity.UpdateCounts(); err != nil {
|
if err = entity.UpdateCounts(); err != nil {
|
||||||
log.Warnf("index: %s (update counts)", err)
|
log.Warnf("index: %s (update counts)", err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Infof("index: found no new or modified files")
|
log.Infof("index: found no new or modified files")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
config.FlushUsageInfoCache()
|
||||||
runtime.GC()
|
runtime.GC()
|
||||||
|
|
||||||
ind.lastRun = entity.Now()
|
ind.lastRun = entity.Now()
|
||||||
|
@@ -27,7 +27,7 @@ func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchive
|
|||||||
Stack: stack,
|
Stack: stack,
|
||||||
FacesOnly: facesOnly,
|
FacesOnly: facesOnly,
|
||||||
SkipArchived: skipArchived,
|
SkipArchived: skipArchived,
|
||||||
ByteLimit: Config().OriginalsByteLimit(),
|
ByteLimit: Config().OriginalsLimitBytes(),
|
||||||
ResolutionLimit: Config().ResolutionLimit(),
|
ResolutionLimit: Config().ResolutionLimit(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -34,7 +34,157 @@ Additional information can be found in our Developer Guide:
|
|||||||
*/
|
*/
|
||||||
package duf
|
package duf
|
||||||
|
|
||||||
// Mounts returns the active file system mounts, along with any warnings or errors that have occurred.
|
import (
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mounts returns all active file system mounts, along with any warnings or errors that have occurred.
|
||||||
func Mounts() (m []Mount, warnings []string, err error) {
|
func Mounts() (m []Mount, warnings []string, err error) {
|
||||||
return mounts()
|
return mounts()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PathInfo returns the closest file system mount for the given path, or an error if not found.
|
||||||
|
func PathInfo(dir string) (Mount, error) {
|
||||||
|
if m, _, err := FindByPath(dir); err != nil {
|
||||||
|
return Mount{}, err
|
||||||
|
} else if len(m) < 1 {
|
||||||
|
return Mount{}, fmt.Errorf("mount not found")
|
||||||
|
} else {
|
||||||
|
return m[0], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByPath returns the active file system mounts for the given path and it's parents, if any.
|
||||||
|
func FindByPath(dir string) (m []Mount, warnings []string, err error) {
|
||||||
|
dir = strings.TrimSpace(dir)
|
||||||
|
|
||||||
|
if dir == "" {
|
||||||
|
return m, warnings, fmt.Errorf("empty path name")
|
||||||
|
}
|
||||||
|
|
||||||
|
folders := strings.Split(dir, "/")
|
||||||
|
|
||||||
|
filter := FilterOptions{}
|
||||||
|
|
||||||
|
if len(folders) <= 1 {
|
||||||
|
filter.OnlyMountPoints = NewFilterValues("/")
|
||||||
|
} else if parent := strings.TrimSpace(folders[1]); parent != "" {
|
||||||
|
filter.OnlyMountPoints = NewFilterValues("/", "/"+parent+"/*")
|
||||||
|
} else if len(folders) > 2 {
|
||||||
|
filter.OnlyMountPoints = NewFilterValues("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
m, warnings, err = Find(filter)
|
||||||
|
|
||||||
|
sort.SliceStable(m, func(i, j int) bool {
|
||||||
|
return strings.Compare(m[i].Mountpoint, m[j].Mountpoint) >= 0
|
||||||
|
})
|
||||||
|
|
||||||
|
return m, warnings, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find returns the active file system mounts that match the filter.
|
||||||
|
func Find(filters FilterOptions) (results []Mount, warnings []string, err error) {
|
||||||
|
m, warnings, err := mounts()
|
||||||
|
|
||||||
|
hasOnlyDevices := len(filters.OnlyDevices) != 0
|
||||||
|
|
||||||
|
_, hideLocal := filters.HiddenDevices[LocalDevice]
|
||||||
|
_, hideNetwork := filters.HiddenDevices[NetworkDevice]
|
||||||
|
_, hideFuse := filters.HiddenDevices[FuseDevice]
|
||||||
|
_, hideSpecial := filters.HiddenDevices[SpecialDevice]
|
||||||
|
_, hideLoops := filters.HiddenDevices[LoopsDevice]
|
||||||
|
_, hideBinds := filters.HiddenDevices[BindsMount]
|
||||||
|
|
||||||
|
_, onlyLocal := filters.OnlyDevices[LocalDevice]
|
||||||
|
_, onlyNetwork := filters.OnlyDevices[NetworkDevice]
|
||||||
|
_, onlyFuse := filters.OnlyDevices[FuseDevice]
|
||||||
|
_, onlySpecial := filters.OnlyDevices[SpecialDevice]
|
||||||
|
_, onlyLoops := filters.OnlyDevices[LoopsDevice]
|
||||||
|
_, onlyBinds := filters.OnlyDevices[BindsMount]
|
||||||
|
|
||||||
|
// sort/filter devices
|
||||||
|
for _, v := range m {
|
||||||
|
if len(filters.OnlyFilesystems) != 0 {
|
||||||
|
// skip not onlyFs
|
||||||
|
if _, ok := filters.OnlyFilesystems[strings.ToLower(v.Fstype)]; !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// skip hideFs
|
||||||
|
if _, ok := filters.HiddenFilesystems[strings.ToLower(v.Fstype)]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip hidden devices
|
||||||
|
if isHiddenFs(v) && !all {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip bind-mounts
|
||||||
|
if strings.Contains(v.Opts, "bind") {
|
||||||
|
if (hasOnlyDevices && !onlyBinds) || (hideBinds && !all) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip loop devices
|
||||||
|
if strings.HasPrefix(v.Device, "/dev/loop") {
|
||||||
|
if (hasOnlyDevices && !onlyLoops) || (hideLoops && !all) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip special devices
|
||||||
|
if v.Blocks == 0 && !all {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip zero size devices
|
||||||
|
if v.BlockSize == 0 && !all {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip not only mount point
|
||||||
|
if len(filters.OnlyMountPoints) != 0 {
|
||||||
|
if !findInKey(v.Mountpoint, filters.OnlyMountPoints) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip hidden mount point
|
||||||
|
if len(filters.HiddenMountPoints) != 0 {
|
||||||
|
if findInKey(v.Mountpoint, filters.HiddenMountPoints) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t := deviceType(v)
|
||||||
|
|
||||||
|
if !all {
|
||||||
|
switch {
|
||||||
|
case hasOnlyDevices && onlyLocal && t != LocalDevice:
|
||||||
|
continue
|
||||||
|
case hasOnlyDevices && onlyNetwork && t != NetworkDevice:
|
||||||
|
continue
|
||||||
|
case hasOnlyDevices && onlyFuse && t != FuseDevice:
|
||||||
|
continue
|
||||||
|
case hasOnlyDevices && onlySpecial && t != SpecialDevice:
|
||||||
|
continue
|
||||||
|
case
|
||||||
|
t == LocalDevice && hideLocal,
|
||||||
|
t == NetworkDevice && hideNetwork,
|
||||||
|
t == FuseDevice && hideFuse,
|
||||||
|
t == SpecialDevice && hideSpecial:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, warnings, err
|
||||||
|
}
|
||||||
|
@@ -12,8 +12,8 @@ const (
|
|||||||
|
|
||||||
var windowsSandboxMountPoints = loadRegisteredWindowsSandboxMountPoints()
|
var windowsSandboxMountPoints = loadRegisteredWindowsSandboxMountPoints()
|
||||||
|
|
||||||
func loadRegisteredWindowsSandboxMountPoints() (ret map[string]struct{}) {
|
func loadRegisteredWindowsSandboxMountPoints() (ret FilterValues) {
|
||||||
ret = make(map[string]struct{})
|
ret = make(FilterValues)
|
||||||
key, err := registry.OpenKey(registry.CURRENT_USER, WindowsSandboxMountPointRegistryPath, registry.READ)
|
key, err := registry.OpenKey(registry.CURRENT_USER, WindowsSandboxMountPointRegistryPath, registry.READ)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
@@ -10,14 +10,32 @@ var (
|
|||||||
onlyMp = ""
|
onlyMp = ""
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type FilterValues map[string]struct{}
|
||||||
|
|
||||||
|
func NewFilterValues(s ...string) FilterValues {
|
||||||
|
if len(s) == 0 {
|
||||||
|
return make(FilterValues)
|
||||||
|
} else if len(s) == 1 {
|
||||||
|
return parseCommaSeparatedValues(s[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(FilterValues, len(s))
|
||||||
|
|
||||||
|
for i := range s {
|
||||||
|
result[s[i]] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
// FilterOptions contains all filters.
|
// FilterOptions contains all filters.
|
||||||
type FilterOptions struct {
|
type FilterOptions struct {
|
||||||
HiddenDevices map[string]struct{}
|
HiddenDevices FilterValues
|
||||||
OnlyDevices map[string]struct{}
|
OnlyDevices FilterValues
|
||||||
|
|
||||||
HiddenFilesystems map[string]struct{}
|
HiddenFilesystems FilterValues
|
||||||
OnlyFilesystems map[string]struct{}
|
OnlyFilesystems FilterValues
|
||||||
|
|
||||||
HiddenMountPoints map[string]struct{}
|
HiddenMountPoints FilterValues
|
||||||
OnlyMountPoints map[string]struct{}
|
OnlyMountPoints FilterValues
|
||||||
}
|
}
|
||||||
|
@@ -8,8 +8,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// parseCommaSeparatedValues parses comma separated string into a map.
|
// parseCommaSeparatedValues parses comma separated string into a map.
|
||||||
func parseCommaSeparatedValues(values string) map[string]struct{} {
|
func parseCommaSeparatedValues(values string) FilterValues {
|
||||||
m := make(map[string]struct{})
|
m := make(FilterValues)
|
||||||
for _, v := range strings.Split(values, ",") {
|
for _, v := range strings.Split(values, ",") {
|
||||||
v = strings.TrimSpace(v)
|
v = strings.TrimSpace(v)
|
||||||
if len(v) == 0 {
|
if len(v) == 0 {
|
||||||
@@ -19,11 +19,12 @@ func parseCommaSeparatedValues(values string) map[string]struct{} {
|
|||||||
v = strings.ToLower(v)
|
v = strings.ToLower(v)
|
||||||
m[v] = struct{}{}
|
m[v] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return m
|
return m
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateGroups validates the parsed group maps.
|
// validateGroups validates the parsed group maps.
|
||||||
func validateGroups(m map[string]struct{}) error {
|
func validateGroups(m FilterValues) error {
|
||||||
for k := range m {
|
for k := range m {
|
||||||
found := false
|
found := false
|
||||||
for _, g := range groups {
|
for _, g := range groups {
|
||||||
@@ -42,7 +43,7 @@ func validateGroups(m map[string]struct{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// findInKey parse a slice of pattern to match the given key.
|
// findInKey parse a slice of pattern to match the given key.
|
||||||
func findInKey(str string, km map[string]struct{}) bool {
|
func findInKey(str string, km FilterValues) bool {
|
||||||
for p := range km {
|
for p := range km {
|
||||||
if wildcard.Match(p, str) {
|
if wildcard.Match(p, str) {
|
||||||
return true
|
return true
|
||||||
|
Reference in New Issue
Block a user