Config: Add option to show filesystem usage in sidebar navigation #4266

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer
2025-03-03 20:05:56 +01:00
parent abfb19bd63
commit c2cc50b670
27 changed files with 469 additions and 55 deletions

View File

@@ -44,6 +44,38 @@ let start = new Date();
const debug = window.__CONFIG__?.debug || window.__CONFIG__?.trace;
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) {
if (!s || !s.length) {
return s;

View File

@@ -137,6 +137,13 @@ export default {
flat: true,
transition: false,
},
VNavigationDrawer: {
width: 270,
railWidth: 70,
mobileBreakpoint: 960,
location: "start",
touchless: true,
},
VListItem: {
ripple: false,
},

View File

@@ -50,14 +50,9 @@
<v-navigation-drawer
v-if="auth"
v-model="drawer"
color="navigation"
:rail="isMini"
:rail-width="70"
:width="270"
:mobile-breakpoint="960"
color="navigation"
class="nav-sidebar navigation"
location="start"
touchless
>
<div class="nav-container">
<v-toolbar flat :density="$vuetify.display.smAndDown ? 'compact' : 'default'">
@@ -760,6 +755,34 @@
</v-list-item>
</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 class="nav-info__underlay"></div>
<div class="text-center my-1">
@@ -935,6 +958,7 @@ export default {
appNameSuffix = appNameParts.slice(1, 9).join(" ");
}
const canManagePhotos = this.$config.allow("photos", "manage");
const isDemo = this.$config.get("demo");
const isPro = !!this.$config.values?.ext["pro"];
const isPublic = this.$config.get("public");
@@ -946,7 +970,7 @@ export default {
return {
canSearchPlaces: this.$config.allow("places", "search"),
canAccessPrivate: !isRestricted && this.$config.allow("photos", "access_private"),
canManagePhotos: this.$config.allow("photos", "manage"),
canManagePhotos: canManagePhotos,
canManagePeople: this.$config.allow("people", "manage"),
canManageUsers: (!isPublic || isDemo) && this.$config.allow("users", "manage"),
appNameSuffix: appNameSuffix,
@@ -960,6 +984,8 @@ export default {
featUpgrade: tier < 6 && isSuperAdmin && !isPublic && !isDemo,
featMembership: tier < 3 && 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,
isMini: localStorage.getItem("last_navigation_mode") !== "false" || isRestricted,
isDemo: isDemo,
@@ -1070,14 +1096,17 @@ export default {
this.isMini = !this.isMini;
localStorage.setItem("last_navigation_mode", `${this.isMini}`);
},
showAccountSettings: function () {
showAccountSettings() {
if (this.$config.feature("account")) {
this.$router.push({ name: "settings_account" });
} else {
this.$router.push({ name: "settings" });
}
},
showServerConnectionHelp: function () {
showUsageInfo() {
this.$router.push({ path: "/index/files" });
},
showServerConnectionHelp() {
this.$router.push({ path: "/help/websockets" });
},
showLegalInfo() {

View File

@@ -176,7 +176,9 @@
<td>
{{ $gettext(`Size`) }}
</td>
<td>{{ file.sizeInfo() }}</td>
<td>
<span v-tooltip="Math.ceil(file?.Size / 1024).toLocaleString() + ' KB'">{{ file.sizeInfo() }}</span>
</td>
</tr>
<tr v-if="file.Software">
<td>

View File

@@ -348,6 +348,13 @@ nav .v-list__item__title.title {
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 {
opacity: var(--v-hover-opacity, .018);
}

View File

@@ -252,14 +252,8 @@ export class File extends RestModel {
info.push(this.Width + " × " + this.Height);
}
if (this.Size > 102400) {
const size = Number.parseFloat(this.Size) / 1048576;
info.push(size.toFixed(1) + " MB");
} else if (this.Size) {
const size = Number.parseFloat(this.Size) / 1024;
info.push(size.toFixed(1) + " KB");
if (this.Size) {
info.push($util.formatBytes(this.Size));
}
}

View File

@@ -875,13 +875,7 @@ export class Photo extends RestModel {
return;
}
if (file.Size > 102400) {
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");
}
info.push($util.formatBytes(file.Size));
}
vectorFile() {

View File

@@ -7,6 +7,18 @@ let chai = require("chai/chai");
let assert = chai.assert;
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", () => {
const duration = $util.formatDuration(-3);
assert.equal(duration, "3ns");

View File

@@ -347,7 +347,7 @@ describe("model/file", () => {
UpdatedAt: "2012-07-08T14:45:39Z",
};
const file = new File(values);
assert.equal(file.sizeInfo(), "7.8 KB");
assert.equal(file.sizeInfo(), "8 KB");
const values2 = {
InstanceID: 5,
UID: "ABC123",
@@ -359,7 +359,7 @@ describe("model/file", () => {
UpdatedAt: "2012-07-08T14:45:39Z",
};
const file2 = new File(values2);
assert.equal(file2.sizeInfo(), "7638.9 MB");
assert.equal(file2.sizeInfo(), "7.5 GB");
const values3 = {
InstanceID: 5,
UID: "ABC123",
@@ -373,7 +373,7 @@ describe("model/file", () => {
UpdatedAt: "2012-07-08T14:45:39Z",
};
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", () => {

View File

@@ -1095,7 +1095,7 @@ describe("model/photo", () => {
],
};
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 = {
ID: 10,
UID: "ABC127",
@@ -1122,7 +1122,7 @@ describe("model/photo", () => {
],
};
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");
});

View File

@@ -91,6 +91,7 @@ type ClientConfig struct {
Categories CategoryLabels `json:"categories"`
Clip int `json:"clip"`
Server env.Resources `json:"server"`
Usage Usage `json:"usage"`
Settings *customize.Settings `json:"settings,omitempty"`
ACL acl.Grants `json:"acl,omitempty"`
Ext Map `json:"ext"`
@@ -510,6 +511,7 @@ func (c *Config) ClientUser(withSettings bool) *ClientConfig {
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
Server: env.Info(),
Usage: c.Usage(),
Ext: ClientExt(c, ClientUser),
}

View File

@@ -663,8 +663,8 @@ func (c *Config) OriginalsLimit() int {
return c.options.OriginalsLimit
}
// OriginalsByteLimit returns the maximum size of originals in bytes.
func (c *Config) OriginalsByteLimit() int64 {
// OriginalsLimitBytes returns the maximum size of originals in bytes.
func (c *Config) OriginalsLimitBytes() int64 {
if result := c.OriginalsLimit(); result <= 0 {
return -1
} else {

View File

@@ -339,12 +339,12 @@ func TestConfig_OriginalsLimit(t *testing.T) {
assert.Equal(t, 800, c.OriginalsLimit())
}
func TestConfig_OriginalsByteLimit(t *testing.T) {
func TestConfig_OriginalsLimitBytes(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, int64(-1), c.OriginalsByteLimit())
assert.Equal(t, int64(-1), c.OriginalsLimitBytes())
c.options.OriginalsLimit = 800
assert.Equal(t, int64(838860800), c.OriginalsByteLimit())
assert.Equal(t, int64(838860800), c.OriginalsLimitBytes())
}
func TestConfig_ResolutionLimit(t *testing.T) {

View 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
}

View 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())
}

View File

@@ -267,6 +267,23 @@ var Flags = CliFlags{
Usage: "create YAML sidecar files to back up picture metadata",
EnvVars: EnvVars("SIDECAR_YAML"),
}, 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{
Name: "backup-path",
Aliases: []string{"ba"},

View File

@@ -26,7 +26,7 @@ type Options struct {
PartnerID string `yaml:"-" json:"-" flag:"partner-id"`
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
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"`
AdminPassword string `yaml:"AdminPassword" json:"-" flag:"admin-password"`
PasswordLength int `yaml:"PasswordLength" json:"-" flag:"password-length"`
@@ -72,6 +72,9 @@ type Options struct {
CustomAssetsPath string `yaml:"-" json:"-" flag:"custom-assets-path"`
SidecarPath string `yaml:"SidecarPath" json:"-" flag:"sidecar-path"`
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"`
BackupSchedule string `yaml:"BackupSchedule" json:"BackupSchedule" flag:"backup-schedule"`
BackupRetain int `yaml:"BackupRetain" json:"BackupRetain" flag:"backup-retain"`

View File

@@ -79,6 +79,11 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"sidecar-path", c.SidecarPath()},
{"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.
{"backup-path", c.BackupBasePath()},
{"backup-schedule", c.BackupSchedule()},

View File

@@ -19,6 +19,7 @@ var OptionsReportSections = []ReportSection{
{Start: "PHOTOPRISM_LOG_LEVEL", Title: "Logging"},
{Start: "PHOTOPRISM_CONFIG_PATH", Title: "Storage"},
{Start: "PHOTOPRISM_SIDECAR_PATH", Title: "Sidecar Files"},
{Start: "PHOTOPRISM_USAGE_INFO", Title: "Usage"},
{Start: "PHOTOPRISM_BACKUP_PATH", Title: "Backup"},
{Start: "PHOTOPRISM_INDEX_WORKERS, PHOTOPRISM_WORKERS", Title: "Indexing"},
{Start: "PHOTOPRISM_READONLY", Title: "Feature Flags"},

View File

@@ -25,7 +25,7 @@ func CachedAlbumByUID(uid string) (m Album, err error) {
}
// Cached?
if cacheData, ok := albumCache.Get(uid); ok {
if cacheData, hit := albumCache.Get(uid); hit {
log.Tracef("album: cache hit for %s", uid)
return cacheData.(Album), nil
}

View File

@@ -254,7 +254,7 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
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())
}
}
@@ -268,16 +268,17 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
// Run face recognition if enabled.
if w := NewFaces(imp.conf); w.Disabled() {
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)
}
// Update photo counts and visibilities.
if err := entity.UpdateCounts(); err != nil {
if err = entity.UpdateCounts(); err != nil {
log.Warnf("index: %s (update counts)", err)
}
}
config.FlushUsageInfoCache()
runtime.GC()
return done

View File

@@ -321,13 +321,14 @@ func (ind *Index) Start(o IndexOptions) (found fs.Done, updated int) {
})
// 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)
}
} else {
log.Infof("index: found no new or modified files")
}
config.FlushUsageInfoCache()
runtime.GC()
ind.lastRun = entity.Now()

View File

@@ -27,7 +27,7 @@ func NewIndexOptions(path string, rescan, convert, stack, facesOnly, skipArchive
Stack: stack,
FacesOnly: facesOnly,
SkipArchived: skipArchived,
ByteLimit: Config().OriginalsByteLimit(),
ByteLimit: Config().OriginalsLimitBytes(),
ResolutionLimit: Config().ResolutionLimit(),
}

View File

@@ -34,7 +34,157 @@ Additional information can be found in our Developer Guide:
*/
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) {
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
}

View File

@@ -12,8 +12,8 @@ const (
var windowsSandboxMountPoints = loadRegisteredWindowsSandboxMountPoints()
func loadRegisteredWindowsSandboxMountPoints() (ret map[string]struct{}) {
ret = make(map[string]struct{})
func loadRegisteredWindowsSandboxMountPoints() (ret FilterValues) {
ret = make(FilterValues)
key, err := registry.OpenKey(registry.CURRENT_USER, WindowsSandboxMountPointRegistryPath, registry.READ)
if err != nil {
return

View File

@@ -10,14 +10,32 @@ var (
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.
type FilterOptions struct {
HiddenDevices map[string]struct{}
OnlyDevices map[string]struct{}
HiddenDevices FilterValues
OnlyDevices FilterValues
HiddenFilesystems map[string]struct{}
OnlyFilesystems map[string]struct{}
HiddenFilesystems FilterValues
OnlyFilesystems FilterValues
HiddenMountPoints map[string]struct{}
OnlyMountPoints map[string]struct{}
HiddenMountPoints FilterValues
OnlyMountPoints FilterValues
}

View File

@@ -8,8 +8,8 @@ import (
)
// parseCommaSeparatedValues parses comma separated string into a map.
func parseCommaSeparatedValues(values string) map[string]struct{} {
m := make(map[string]struct{})
func parseCommaSeparatedValues(values string) FilterValues {
m := make(FilterValues)
for _, v := range strings.Split(values, ",") {
v = strings.TrimSpace(v)
if len(v) == 0 {
@@ -19,11 +19,12 @@ func parseCommaSeparatedValues(values string) map[string]struct{} {
v = strings.ToLower(v)
m[v] = struct{}{}
}
return m
}
// validateGroups validates the parsed group maps.
func validateGroups(m map[string]struct{}) error {
func validateGroups(m FilterValues) error {
for k := range m {
found := false
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.
func findInKey(str string, km map[string]struct{}) bool {
func findInKey(str string, km FilterValues) bool {
for p := range km {
if wildcard.Match(p, str) {
return true