mirror of
https://github.com/photoprism/photoprism.git
synced 2025-10-08 18:20:55 +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;
|
||||
|
||||
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;
|
||||
|
@@ -137,6 +137,13 @@ export default {
|
||||
flat: true,
|
||||
transition: false,
|
||||
},
|
||||
VNavigationDrawer: {
|
||||
width: 270,
|
||||
railWidth: 70,
|
||||
mobileBreakpoint: 960,
|
||||
location: "start",
|
||||
touchless: true,
|
||||
},
|
||||
VListItem: {
|
||||
ripple: false,
|
||||
},
|
||||
|
@@ -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() {
|
||||
|
@@ -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>
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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() {
|
||||
|
@@ -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");
|
||||
|
@@ -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", () => {
|
||||
|
@@ -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");
|
||||
});
|
||||
|
||||
|
@@ -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),
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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) {
|
||||
|
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",
|
||||
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"},
|
||||
|
@@ -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"`
|
||||
|
@@ -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()},
|
||||
|
@@ -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"},
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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()
|
||||
|
@@ -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(),
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user