Config: Automatically generate command-flag docs #1017 #2195 #2227 #2250

This commit is contained in:
Michael Mayer 2022-04-22 17:38:40 +02:00
parent 0c345d4426
commit 8638929d84
52 changed files with 1332 additions and 647 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

View file

@ -4,7 +4,7 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
<meta name="viewport" content="width=device-width, initial-scale=1.0{{if not .config.Settings.UI.Zoom }}, maximum-scale=1.0, user-scalable=no{{end}}">
<title>{{ .config.SiteTitle }}</title>
<title>{{if and .config.SiteCaption .config.Sponsor }}{{ .config.SiteCaption }}{{else}}{{ .config.Name }}{{end}}</title>
<meta property="og:url" content="{{ .config.SiteUrl }}">
<meta property="og:type" content="website">

View file

@ -28,10 +28,11 @@ import (
"os"
"path/filepath"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/commands"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
"github.com/urfave/cli"
)
var version = "development"
@ -55,7 +56,7 @@ func main() {
app.Version = version
app.Copyright = appCopyright
app.EnableBashCompletion = true
app.Flags = config.GlobalFlags
app.Flags = config.Flags.Cli()
app.Commands = commands.PhotoPrism
if err := app.Run(os.Args); err != nil {

View file

@ -17,7 +17,6 @@ services:
- "~/.cache/go-mod:/go/pkg/mod"
environment:
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"

View file

@ -24,7 +24,6 @@ services:
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial "admin" password (minimum 8 characters)
## Public server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://latest.localssl.dev/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"

View file

@ -24,7 +24,6 @@ services:
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial "admin" password (minimum 8 characters)
## Public server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://latest.localssl.dev/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"

View file

@ -26,7 +26,6 @@ services:
shm_size: "2gb"
environment:
PHOTOPRISM_SITE_URL: "http://localhost:2342/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Open-Source Photo Management"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"

View file

@ -36,7 +36,6 @@ services:
PHOTOPRISM_ADMIN_PASSWORD: "photoprism" # initial "admin" password (minimum 8 characters)
## External development server URL incl http:// or https:// and /path, :port is optional
PHOTOPRISM_SITE_URL: "https://app.localssl.dev/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: "Tags and finds pictures without getting in your way!"
PHOTOPRISM_SITE_AUTHOR: "@photoprism_app"

View file

@ -86,7 +86,6 @@ services:
PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB or MySQL database schema name
PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB or MySQL database user name
PHOTOPRISM_DATABASE_PASSWORD: "insecure" # MariaDB or MySQL database user password
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -81,7 +81,6 @@ services:
PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB or MySQL database schema name
PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB or MySQL database user name
PHOTOPRISM_DATABASE_PASSWORD: "insecure" # MariaDB or MySQL database user password
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -129,7 +129,6 @@ services:
environment:
## !! CHANGE site url if your server has a public domain name e.g. "https://photos.yourdomain.com/" !!
PHOTOPRISM_SITE_URL: "https://_public_ip_/"
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -77,7 +77,6 @@ services:
PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB or MySQL database schema name
PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB or MySQL database user name
PHOTOPRISM_DATABASE_PASSWORD: "insecure" # MariaDB or MySQL database user password
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -73,7 +73,6 @@ services:
PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB or MySQL database schema name
PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB or MySQL database user name
PHOTOPRISM_DATABASE_PASSWORD: "insecure" # MariaDB or MySQL database user password
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -79,7 +79,6 @@ services:
PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB or MySQL database schema name
PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB or MySQL database user name
PHOTOPRISM_DATABASE_PASSWORD: "insecure" # MariaDB or MySQL database user password
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -72,7 +72,6 @@ services:
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
PHOTOPRISM_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
PHOTOPRISM_DATABASE_DRIVER: "sqlite" # SQLite is an embedded database that doesn't require a server
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -78,7 +78,6 @@ services:
PHOTOPRISM_DATABASE_NAME: "photoprism" # MariaDB or MySQL database schema name
PHOTOPRISM_DATABASE_USER: "photoprism" # MariaDB or MySQL database user name
PHOTOPRISM_DATABASE_PASSWORD: "insecure" # MariaDB or MySQL database user password
PHOTOPRISM_SITE_TITLE: "PhotoPrism"
PHOTOPRISM_SITE_CAPTION: "AI-Powered Photos App"
PHOTOPRISM_SITE_DESCRIPTION: ""
PHOTOPRISM_SITE_AUTHOR: ""

View file

@ -44,7 +44,6 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_TITLE="PhotoPrism" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \

View file

@ -39,7 +39,6 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_TITLE="PhotoPrism" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \

View file

@ -39,7 +39,6 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_TITLE="PhotoPrism" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \

View file

@ -44,7 +44,6 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_TITLE="PhotoPrism" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \

View file

@ -44,7 +44,6 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_TITLE="PhotoPrism" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \

View file

@ -44,7 +44,6 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \
PHOTOPRISM_DETECT_NSFW="false" \
PHOTOPRISM_EXPERIMENTAL="false" \
PHOTOPRISM_SITE_URL="http://localhost:2342/" \
PHOTOPRISM_SITE_TITLE="PhotoPrism" \
PHOTOPRISM_SITE_CAPTION="AI-Powered Photos App" \
PHOTOPRISM_SITE_DESCRIPTION="" \
PHOTOPRISM_SITE_AUTHOR="" \

View file

@ -4402,9 +4402,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.117",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.117.tgz",
"integrity": "sha512-ypZHxY+Sf/PXu7LVN+xoeanyisnJeSOy8Ki439L/oLueZb4c72FI45zXcK3gPpmTwyufh9m6NnbMLXnJh/0Fxg=="
"version": "1.4.118",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.118.tgz",
"integrity": "sha512-maZIKjnYDvF7Fs35nvVcyr44UcKNwybr93Oba2n3HkKDFAtk0svERkLN/HyczJDS3Fo4wU9th9fUQd09ZLtj1w=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -11753,9 +11753,9 @@
}
},
"node_modules/tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"node_modules/tsscmp": {
"version": "1.0.6",
@ -16059,9 +16059,9 @@
}
},
"electron-to-chromium": {
"version": "1.4.117",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.117.tgz",
"integrity": "sha512-ypZHxY+Sf/PXu7LVN+xoeanyisnJeSOy8Ki439L/oLueZb4c72FI45zXcK3gPpmTwyufh9m6NnbMLXnJh/0Fxg=="
"version": "1.4.118",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.118.tgz",
"integrity": "sha512-maZIKjnYDvF7Fs35nvVcyr44UcKNwybr93Oba2n3HkKDFAtk0svERkLN/HyczJDS3Fo4wU9th9fUQd09ZLtj1w=="
},
"emoji-regex": {
"version": "8.0.0",
@ -21324,9 +21324,9 @@
}
},
"tslib": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz",
"integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz",
"integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ=="
},
"tsscmp": {
"version": "1.0.6",

View file

@ -165,12 +165,22 @@ router.beforeEach((to, from, next) => {
});
router.afterEach((to) => {
if (to.meta.title && config.values.siteTitle !== to.meta.title) {
config.page.title = $gettext(to.meta.title);
window.document.title = config.values.siteTitle + ": " + config.page.title;
const t = to.meta["title"] ? to.meta["title"] : "";
if (t !== "" && config.values.siteTitle !== t && config.values.name !== t) {
config.page.title = $gettext(t);
if (config.page.title === "") {
window.document.title = config.values.siteTitle;
} else {
window.document.title = config.page.title + " " + config.values.siteTitle;
}
} else {
config.page.title = config.values.siteTitle;
window.document.title = config.values.siteTitle + ": " + config.values.siteCaption;
config.page.title = config.values.name;
if (config.values.siteCaption === "" || !config.values.sponsor) {
window.document.title = config.values.siteTitle;
} else {
window.document.title = config.values.siteCaption;
}
}
});

View file

@ -2,16 +2,18 @@
<v-card flat tile class="ma-0 pa-0 application p-about-footer">
<v-card-actions class="px-4 py-2">
<v-layout wrap align-top pt-3>
<v-flex xs12 sm6 class="px-0 pb-2 body-1 text-selectable text-xs-left">
Build {{ $config.get("version") }}<br>
<v-flex xs12 sm6 class="px-0 pb-2 body-1 text-selectable text-xs-center text-sm-left">
<template v-if="sponsor"><router-link to="/about" class="text-link"><translate>Thank you for supporting PhotoPrism®</translate></router-link></template>
<strong v-else><router-link to="/about" class="text-link"><translate>PhotoPrism® needs your support</translate></router-link></strong>
<br><a href="https://docs.photoprism.app/release-notes/" target="_blank">Build {{ $config.get("version") }}</a>
</v-flex>
<v-flex xs12 sm6 class="px-0 pb-2 body-1 text-xs-left text-sm-right">
<a href="https://photoprism.app/team/" target="_blank">© 2018-2022 PhotoPrism UG</a><br>
<a href="https://raw.githubusercontent.com/photoprism/photoprism/develop/NOTICE"
target="_blank" class="text-link">3rd-party software packages</a>
<v-flex xs12 sm6 class="px-0 pb-2 body-1 text-xs-center text-sm-right">
<span class="hidden-sm-and-down">
<a href="https://raw.githubusercontent.com/photoprism/photoprism/develop/NOTICE"
target="_blank" class="text-link">3rd-party software packages</a><br>
</span>
<a href="https://photoprism.app/team/" target="_blank">© 2018-2022 PhotoPrism UG</a>
</v-flex>
</v-layout>
</v-card-actions>

View file

@ -86,6 +86,10 @@ footer {
padding: 1rem 2rem;
}
#photoprism .p-about-footer .body-1 {
line-height: 1.8em;
}
main {
padding: 0;
margin: 0;

View file

@ -283,6 +283,7 @@ export const MapsStyle = () => [
{
text: $gettext("Streets"),
value: "streets",
sponsor: true,
},
{
text: $gettext("Hybrid"),

View file

@ -2,7 +2,7 @@
<div class="p-page p-page-about">
<v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown">
<v-toolbar-title>
<translate>About</translate>
<translate>About</translate> {{ $config.get('name') }}
</v-toolbar-title>
<v-spacer></v-spacer>

View file

@ -1,5 +1,5 @@
<template>
<v-container fluid fill-height class="auth-login wallpaper pa-3">
<v-container fluid fill-height class="auth-login wallpaper pa-3" :style="wallpaper()">
<v-layout align-center justify-center>
<v-flex xs12 sm8 md4 xl3 xxl2>
<v-form ref="form" dense class="auth-login-form" accept-charset="UTF-8" @submit.prevent="login">
@ -56,17 +56,38 @@
</v-form>
</v-flex>
</v-layout>
<footer>
<footer v-if="sponsor">
<v-layout wrap align-top pa-0 ma-0>
<v-flex xs12 class="pa-0 body-2 text-selectable text-xs-center white--text" :class="[config.imprint ? 'text-sm-left sm6' : '']">
<strong>{{ config.siteTitle }}</strong> {{ config.siteCaption }}
<v-flex xs12 class="pa-0 body-2 text-selectable text-xs-center white--text"
:class="[config.imprint ? 'text-sm-left sm6' : '']">
<strong>{{ config.siteCaption ? config.siteCaption : config.siteTitle }}</strong>
</v-flex>
<v-flex v-if="config.imprint" xs12 sm6 class="pa-0 body-2 text-xs-center text-sm-right white--text">
<a v-if="config.imprintUrl" :href="config.imprintUrl" target="_blank" class="text-link" :style="`color: ${colors.link}!important`">{{ config.imprint }}</a>
<a v-if="config.imprintUrl" :href="config.imprintUrl" target="_blank" class="text-link"
:style="`color: ${colors.link}!important`">{{ config.imprint }}</a>
<span v-else>{{ config.imprint }}</span>
</v-flex>
</v-layout>
</footer>
<footer v-else>
<v-layout wrap align-top pa-0 ma-0>
<v-flex xs12 sm6 class="pa-0 body-2 text-xs-center text-sm-left white--text text-selectable">
<strong>{{ config.siteTitle }}</strong> {{ config.siteCaption }}
</v-flex>
<v-flex xs12 sm6 class="pa-0 body-2 text-xs-center text-sm-right white--text">
<v-btn
href="https://link.photoprism.app/patreon"
target="_blank"
color="transparent"
class="white--text px-3 py-2 ma-0 action-sponsor"
round depressed small
>
<translate>Become a sponsor</translate>
<v-icon :left="rtl" :right="!rtl" size="16" class="ml-2" dark>star</v-icon>
</v-btn>
</v-flex>
</v-layout>
</footer>
</v-container>
</template>
@ -76,6 +97,7 @@ export default {
name: "PPageAuthLogin",
data() {
const c = this.$config.values;
const sponsor = this.$config.isSponsor();
return {
colors: {
@ -85,12 +107,13 @@ export default {
},
loading: false,
showPassword: false,
username: "",
username: sponsor ? "" : "admin",
password: "",
sponsor: this.$config.isSponsor(),
sponsor: sponsor,
config: this.$config.values,
siteDescription: c.siteDescription ? c.siteDescription : c.siteCaption,
nextUrl: this.$route.params.nextUrl ? this.$route.params.nextUrl : "/",
wallpaperUri: c.wallpaperUri,
rtl: this.$rtl,
};
},
@ -106,6 +129,13 @@ export default {
this.$scrollbar.show();
},
methods: {
wallpaper() {
if (this.wallpaperUri) {
return `background-image: url(${this.wallpaperUri});`;
}
return "";
},
login() {
if (!this.username || !this.password) {
return;

View file

@ -9,6 +9,7 @@ var ShowCommand = cli.Command{
Name: "show",
Usage: "Configuration and system report subcommands",
Subcommands: []cli.Command{
ShowFlagsCommand,
ShowConfigCommand,
ShowTagsCommand,
ShowFiltersCommand,

View file

@ -0,0 +1,119 @@
package commands
import (
"fmt"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/report"
)
// ShowFlagsCommand configures the command name, flags, and action.
var ShowFlagsCommand = cli.Command{
Name: "flags",
Usage: "Shows environment variable command-line parameter names",
Flags: report.CliFlags,
Action: showFlagsAction,
}
var faceFlagsInfo = `!!! info ""
To [recognize faces](../user-guide/organize/people.md), PhotoPrism first extracts crops from your images using a
[library](https://github.com/esimov/pigo) based on [pixel intensity comparisons](https://arxiv.org/pdf/1305.4537.pdf).
These are then fed into TensorFlow to compute [512-dimensional vectors](https://www.cv-foundation.org/openaccess/content_cvpr_2015/papers/Schroff_FaceNet_A_Unified_2015_CVPR_paper.pdf)
for characterization. In the final step, the [DBSCAN algorithm](https://en.wikipedia.org/wiki/DBSCAN)
attempts to cluster these so-called face embeddings, so they can be matched to persons with just a few clicks.
A reasonable range for the similarity distance between face embeddings is between 0.60 and 0.70, with a higher
value being more aggressive and leading to larger clusters with more false positives.
To cluster a smaller number of faces, you can reduce the core to 3 or 2 similar faces.
We recommend that only advanced users change these parameters:`
// showFlagsAction shows environment variable command-line parameter names.
func showFlagsAction(ctx *cli.Context) error {
conf := config.NewConfig(ctx)
conf.SetLogLevel(logrus.FatalLevel)
rows, cols := config.Flags.Report()
// CSV Export?
if ctx.Bool("csv") || ctx.Bool("tsv") {
result, err := report.Render(rows, cols, report.CliFormat(ctx))
fmt.Println(result)
return err
}
type Section struct {
Start string
Caption string
Info string
}
s := []Section{
{Start: "PHOTOPRISM_ADMIN_PASSWORD", Caption: "Authentication"},
{Start: "PHOTOPRISM_LOG_LEVEL", Caption: "Logging"},
{Start: "PHOTOPRISM_CONFIG_PATH", Caption: "Storage"},
{Start: "PHOTOPRISM_WORKERS", Caption: "Index Workers"},
{Start: "PHOTOPRISM_READONLY", Caption: "Feature Flags"},
{Start: "PHOTOPRISM_DEFAULT_LOCALE", Caption: "Customization"},
{Start: "PHOTOPRISM_CDN_URL", Caption: "Site Information"},
{Start: "PHOTOPRISM_HTTP_PORT", Caption: "Web Server"},
{Start: "PHOTOPRISM_DATABASE_DRIVER", Caption: "Database Connection"},
{Start: "PHOTOPRISM_DARKTABLE_BIN", Caption: "File Converters"},
{Start: "PHOTOPRISM_DOWNLOAD_TOKEN", Caption: "Security Tokens"},
{Start: "PHOTOPRISM_THUMB_COLOR", Caption: "Image Quality"},
{Start: "PHOTOPRISM_FACE_SIZE", Caption: "Face Recognition",
Info: faceFlagsInfo},
{Start: "PHOTOPRISM_PID_FILENAME", Caption: "Daemon Mode",
Info: "If you start the server as a *daemon* in the background, you can additionally specify a filename for the log and the process ID:"},
}
j := 0
for i, sec := range s {
fmt.Printf("### %s ###\n\n", sec.Caption)
if sec.Info != "" && ctx.Bool("md") {
fmt.Printf("%s\n\n", sec.Info)
}
secRows := make([][]string, 0, len(rows))
for {
row := rows[j]
if len(row) < 1 {
continue
}
if i < len(s)-1 {
if s[i+1].Start == row[0] {
break
}
}
secRows = append(secRows, row)
j++
if j >= len(rows) {
break
}
}
result, err := report.Render(secRows, cols, report.CliFormat(ctx))
if err != nil {
return err
}
fmt.Println(result)
if j >= len(rows) {
break
}
}
return nil
}

View file

@ -31,6 +31,7 @@ var UsersCommand = cli.Command{
Name: "add",
Usage: "Adds a new user",
Action: usersAddAction,
Hidden: !config.Sponsor(),
Flags: []cli.Flag{
cli.StringFlag{
Name: "fullname, n",

View file

@ -0,0 +1,66 @@
package config
import (
"reflect"
"github.com/photoprism/photoprism/pkg/list"
"github.com/urfave/cli"
)
// CliFlag represents a command-line parameter.
type CliFlag struct {
Flag cli.DocGenerationFlag
Tags []string
}
// Skip checks if the parameter should be skipped based on a list of tags.
func (f CliFlag) Skip(tags []string) bool {
return len(f.Tags) > 0 && !list.ContainsAny(f.Tags, tags)
}
// Fields returns the flag struct fields.
func (f CliFlag) Fields() reflect.Value {
fields := reflect.ValueOf(f.Flag)
for fields.Kind() == reflect.Ptr {
fields = reflect.Indirect(fields)
}
return fields
}
// Hidden checks if the flag is hidden.
func (f CliFlag) Hidden() bool {
field := f.Fields().FieldByName("Hidden")
if !field.IsValid() || !field.Bool() {
return false
}
return true
}
// EnvVar returns the flag environment variable name.
func (f CliFlag) EnvVar() string {
field := f.Fields().FieldByName("EnvVar")
if !field.IsValid() {
return ""
}
return field.String()
}
// Name returns the command flag name.
func (f CliFlag) Name() string {
return f.Flag.GetName()
}
// Usage returns the command flag usage.
func (f CliFlag) Usage() string {
if list.Contains(f.Tags, EnvSponsor) {
return f.Flag.GetUsage() + "*sponsors only*"
} else {
return f.Flag.GetUsage()
}
}

View file

@ -0,0 +1,66 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli"
)
func TestCliFlag_Skip(t *testing.T) {
withTags := CliFlag{
Flag: cli.StringFlag{
Name: "with-tags",
Usage: "`STRING`",
EnvVar: "PHOTOPRISM_WITH_TAGS",
},
Tags: []string{"foo", "bar"},
}
noTags := CliFlag{
Flag: cli.StringFlag{
Name: "no-tags",
Usage: "`STRING`",
EnvVar: "PHOTOPRISM_NO_TAGS",
},
Tags: []string{},
}
t.Run("True", func(t *testing.T) {
assert.True(t, withTags.Skip([]string{"baz"}))
assert.False(t, noTags.Skip([]string{"baz"}))
})
t.Run("False", func(t *testing.T) {
assert.False(t, withTags.Skip([]string{"foo"}))
assert.False(t, noTags.Skip([]string{"foo"}))
})
}
func TestCliFlag_Hidden(t *testing.T) {
hidden := CliFlag{
Flag: cli.StringFlag{
Name: "is-hidden",
Usage: "`STRING`",
EnvVar: "PHOTOPRISM_HIDDEN",
Hidden: true,
},
Tags: []string{"foo", "bar"},
}
visible := CliFlag{
Flag: cli.StringFlag{
Name: "is-visible",
Usage: "`STRING`",
EnvVar: "PHOTOPRISM_VISIBLE",
Hidden: false,
},
Tags: []string{},
}
t.Run("True", func(t *testing.T) {
assert.True(t, hidden.Hidden())
})
t.Run("False", func(t *testing.T) {
assert.False(t, visible.Hidden())
})
}

View file

@ -0,0 +1,36 @@
package config
import (
"github.com/photoprism/photoprism/pkg/list"
"github.com/urfave/cli"
)
// CliFlags represents a list of command-line parameters.
type CliFlags []CliFlag
// Cli returns the currently active command-line parameters.
func (f CliFlags) Cli() (result []cli.Flag) {
var tags []string
switch {
case Sponsor():
tags = []string{EnvSponsor}
}
return f.Find(tags)
}
// Find finds command-line parameters based on a list of tags.
func (f CliFlags) Find(tags []string) (result []cli.Flag) {
result = make([]cli.Flag, 0, len(f))
for _, flag := range f {
if len(flag.Tags) > 0 && !list.ContainsAny(flag.Tags, tags) {
continue
}
result = append(result, flag.Flag)
}
return result
}

View file

@ -0,0 +1,19 @@
package config
// Report returns global config values as a table for reporting.
func (f CliFlags) Report() (rows [][]string, cols []string) {
cols = []string{"Variable", "Flag", "Usage"}
rows = make([][]string, 0, len(f))
for _, flag := range Flags {
if flag.Hidden() {
continue
}
row := []string{flag.EnvVar(), flag.Name(), flag.Usage()}
rows = append(rows, row)
}
return rows, cols
}

View file

@ -0,0 +1,25 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestCliFlags_Cli(t *testing.T) {
cliFlags := Flags.Cli()
standard := Flags.Find([]string{})
assert.Greater(t, len(cliFlags), len(standard))
}
func TestCliFlags_Find(t *testing.T) {
cliFlags := Flags.Cli()
standard := Flags.Find([]string{})
sponsor := Flags.Find([]string{EnvSponsor})
other := Flags.Find([]string{"other"})
assert.Equal(t, len(standard), len(other))
assert.Equal(t, len(cliFlags), len(sponsor))
assert.Less(t, len(other), len(sponsor))
}

View file

@ -26,6 +26,7 @@ type ClientConfig struct {
ManifestUri string `json:"manifestUri"`
ApiUri string `json:"apiUri"`
ContentUri string `json:"contentUri"`
WallpaperUri string `json:"wallpaperUri"`
SiteUrl string `json:"siteUrl"`
SiteDomain string `json:"siteDomain"`
SiteAuthor string `json:"siteAuthor"`
@ -227,6 +228,7 @@ func (c *Config) PublicConfig() ClientConfig {
AppName: c.AppName(),
AppMode: c.AppMode(),
AppIcon: c.AppIcon(),
WallpaperUri: c.WallpaperUri(),
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),
@ -300,6 +302,7 @@ func (c *Config) GuestConfig() ClientConfig {
AppName: c.AppName(),
AppMode: c.AppMode(),
AppIcon: c.AppIcon(),
WallpaperUri: c.WallpaperUri(),
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),
@ -367,6 +370,7 @@ func (c *Config) UserConfig() ClientConfig {
AppName: c.AppName(),
AppMode: c.AppMode(),
AppIcon: c.AppIcon(),
WallpaperUri: c.WallpaperUri(),
Version: c.Version(),
Copyright: c.Copyright(),
Debug: c.Debug(),

View file

@ -71,14 +71,16 @@ func init() {
}
}
func initLogger(debug bool) {
func initLogger() {
once.Do(func() {
log.SetFormatter(&logrus.TextFormatter{
DisableColors: false,
FullTimestamp: true,
})
if debug {
if Env(EnvTrace) {
log.SetLevel(logrus.TraceLevel)
} else if Env(EnvDebug) {
log.SetLevel(logrus.DebugLevel)
} else {
log.SetLevel(logrus.InfoLevel)
@ -89,7 +91,7 @@ func initLogger(debug bool) {
// NewConfig initialises a new configuration file
func NewConfig(ctx *cli.Context) *Config {
// Initialize logger.
initLogger(ctx.GlobalBool("debug"))
initLogger()
// Initialize options from config file and CLI context.
c := &Config{
@ -291,6 +293,10 @@ func (c *Config) SerialChecksum() string {
// Name returns the application name ("PhotoPrism").
func (c *Config) Name() string {
if c.Sponsor() && c.options.Name == "PhotoPrism" {
c.options.Name = "PhotoPrism+"
}
return c.options.Name
}
@ -331,6 +337,10 @@ func (c *Config) ApiUri() string {
// CdnUrl returns the optional content delivery network URI without trailing slash.
func (c *Config) CdnUrl(res string) string {
if c.NoSponsor() {
return res
}
return strings.TrimRight(c.options.CdnUrl, "/") + res
}
@ -369,7 +379,7 @@ func (c *Config) SiteAuthor() string {
// SiteTitle returns the main site title (default is application name).
func (c *Config) SiteTitle() string {
if c.options.SiteTitle == "" {
if c.options.SiteTitle == "" || c.NoSponsor() {
return c.Name()
}
@ -401,7 +411,7 @@ func (c *Config) SitePreview() string {
// Imprint returns the legal info text for the page footer.
func (c *Config) Imprint() string {
if !c.Sponsor() || c.Test() {
if c.NoSponsor() {
return MsgSponsor
}
@ -410,7 +420,7 @@ func (c *Config) Imprint() string {
// ImprintUrl returns the legal info url.
func (c *Config) ImprintUrl() string {
if !c.Sponsor() || c.Test() {
if c.NoSponsor() {
return SignUpURL
}
@ -422,6 +432,7 @@ func (c *Config) Debug() bool {
if c.Trace() {
return true
}
return c.options.Debug
}
@ -445,6 +456,11 @@ func (c *Config) Sponsor() bool {
return c.options.Sponsor || c.Test()
}
// NoSponsor reports if the instance is not operated by a sponsor.
func (c *Config) NoSponsor() bool {
return !c.Sponsor() && !c.Demo()
}
// Public checks if app runs in public mode and requires no authentication.
func (c *Config) Public() bool {
if c.Auth() {

View file

@ -1,18 +1,20 @@
package config
import (
"path"
"path/filepath"
"strings"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/pkg/clean"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
// DefaultTheme returns the default user interface theme name.
func (c *Config) DefaultTheme() string {
if c.options.DefaultTheme == "" || !c.Sponsor() {
if c.options.DefaultTheme == "" || c.NoSponsor() {
return "default"
}
@ -32,7 +34,7 @@ func (c *Config) DefaultLocale() string {
func (c *Config) AppIcon() string {
defaultIcon := "logo"
if c.options.AppIcon == "" || c.options.AppIcon == defaultIcon {
if c.NoSponsor() || c.options.AppIcon == "" || c.options.AppIcon == defaultIcon {
// Default.
} else if fs.FileExists(c.AppIconsPath(c.options.AppIcon, "512.png")) {
return c.options.AppIcon
@ -44,9 +46,9 @@ func (c *Config) AppIcon() string {
// AppIconsPath returns the path to the app icons.
func (c *Config) AppIconsPath(name ...string) string {
if len(name) > 0 {
folder := []string{c.StaticPath(), "icons"}
folder = append(folder, name...)
return filepath.Join(folder...)
filePath := []string{c.StaticPath(), "icons"}
filePath = append(filePath, name...)
return filepath.Join(filePath...)
}
return filepath.Join(c.StaticPath(), "icons")
@ -56,7 +58,7 @@ func (c *Config) AppIconsPath(name ...string) string {
func (c *Config) AppName() string {
name := strings.TrimSpace(c.options.AppName)
if name == "" {
if c.NoSponsor() || name == "" {
name = c.SiteTitle()
}
@ -83,3 +85,36 @@ func (c *Config) AppMode() string {
return "standalone"
}
}
// WallpaperUri returns the login screen background image `URI`.
func (c *Config) WallpaperUri() string {
if c.NoSponsor() {
return ""
} else if strings.Contains(c.options.WallpaperUri, "/") {
return c.options.WallpaperUri
}
assetPath := "img/wallpaper"
// Empty URI?
if c.options.WallpaperUri == "" {
if !fs.PathExists(filepath.Join(c.StaticPath(), assetPath)) {
return ""
}
c.options.WallpaperUri = "default.jpg"
} else if !strings.Contains(c.options.WallpaperUri, ".") {
c.options.WallpaperUri += fs.ExtJPEG
}
// Valid URI? Local file?
if p := clean.Path(c.options.WallpaperUri); p == "" {
return ""
} else if fs.FileExists(filepath.Join(c.StaticPath(), assetPath, p)) {
c.options.WallpaperUri = path.Join(c.StaticUri(), assetPath, p)
} else {
c.options.WallpaperUri = ""
}
return c.options.WallpaperUri
}

View file

@ -11,11 +11,20 @@ func TestConfig_DefaultTheme(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "default", c.DefaultTheme())
c.options.Demo = false
c.options.Sponsor = false
c.options.Test = false
c.options.DefaultTheme = "grayscale"
assert.Equal(t, "default", c.DefaultTheme())
c.options.Sponsor = true
assert.Equal(t, "grayscale", c.DefaultTheme())
c.options.Sponsor = false
c.options.Test = true
assert.Equal(t, "grayscale", c.DefaultTheme())
c.options.Sponsor = false
c.options.Test = false
assert.Equal(t, "default", c.DefaultTheme())
c.options.Sponsor = true
c.options.DefaultTheme = ""
assert.Equal(t, "default", c.DefaultTheme())
c.options.Sponsor = false
@ -77,3 +86,19 @@ func TestConfig_AppMode(t *testing.T) {
assert.Equal(t, "standalone", c.AppMode())
}
func TestConfig_WallpaperUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.WallpaperUri())
c.options.WallpaperUri = "kashmir"
assert.Equal(t, "/static/img/wallpaper/kashmir.jpg", c.WallpaperUri())
c.options.WallpaperUri = "https://cdn.photoprism.app/wallpaper/welcome.jpg"
assert.Equal(t, "https://cdn.photoprism.app/wallpaper/welcome.jpg", c.WallpaperUri())
c.options.Test = false
assert.Equal(t, "", c.WallpaperUri())
c.options.Test = true
assert.Equal(t, "https://cdn.photoprism.app/wallpaper/welcome.jpg", c.WallpaperUri())
c.options.WallpaperUri = ""
assert.Equal(t, "", c.WallpaperUri())
}

View file

@ -1,5 +1,10 @@
package config
// Sponsor checks if sponsor features should be enabled.
func Sponsor() bool {
return Env(EnvDemo, EnvSponsor, EnvTest)
}
// DisableWebDAV checks if the built-in WebDAV server should be disabled.
func (c *Config) DisableWebDAV() bool {
if c.ReadOnly() || c.Demo() {

View file

@ -12,14 +12,15 @@ func (c *Config) Report() (rows [][]string, cols []string) {
cols = []string{"Value", "Name"}
rows = [][]string{
// Authentication.
{"admin-password", strings.Repeat("*", utf8.RuneCountInString(c.AdminPassword()))},
{"auth", fmt.Sprintf("%t", c.Auth())},
{"public", fmt.Sprintf("%t", c.Public())},
// Logging.
{"log-level", c.LogLevel().String()},
{"debug", fmt.Sprintf("%t", c.Debug())},
{"trace", fmt.Sprintf("%t", c.Trace())},
{"auth", fmt.Sprintf("%t", c.Auth())},
{"public", fmt.Sprintf("%t", c.Public())},
{"read-only", fmt.Sprintf("%t", c.ReadOnly())},
{"experimental", fmt.Sprintf("%t", c.Experimental())},
// Config.
{"config-path", c.ConfigPath()},
@ -55,6 +56,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"auto-import", fmt.Sprintf("%d", c.AutoImport()/time.Second)},
// Feature Flags.
{"read-only", fmt.Sprintf("%t", c.ReadOnly())},
{"experimental", fmt.Sprintf("%t", c.Experimental())},
{"disable-backups", fmt.Sprintf("%t", c.DisableBackups())},
{"disable-settings", fmt.Sprintf("%t", c.DisableSettings())},
{"disable-places", fmt.Sprintf("%t", c.DisablePlaces())},
@ -79,13 +82,13 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"tensorflow-version", c.TensorFlowVersion()},
{"tensorflow-model-path", c.TensorFlowModelPath()},
// UI Defaults.
// Customization.
{"default-locale", c.DefaultLocale()},
// Progressive Web App.
{"default-theme", c.DefaultTheme()},
{"app-icon", c.AppIcon()},
{"app-name", c.AppName()},
{"app-mode", c.AppMode()},
{"wallpaper-uri", c.WallpaperUri()},
// Site Infos.
{"cdn-url", c.CdnUrl("/")},

View file

@ -12,6 +12,7 @@ import (
)
func TestMain(m *testing.M) {
_ = os.Setenv("PHOTOPRISM_TEST", "true")
log = logrus.StandardLogger()
log.SetLevel(logrus.TraceLevel)

28
internal/config/env.go Normal file
View file

@ -0,0 +1,28 @@
package config
import (
"os"
"strings"
"github.com/photoprism/photoprism/pkg/list"
)
// Environment names.
const (
EnvDebug = "debug"
EnvTrace = "trace"
EnvDemo = "demo"
EnvSponsor = "sponsor"
EnvTest = "test"
)
// Env checks the presence of environment and command-line flags.
func Env(vars ...string) bool {
for _, s := range vars {
if os.Getenv("PHOTOPRISM_"+strings.ToUpper(s)) == "true" || list.Contains(os.Args, "--"+s) {
return true
}
}
return false
}

View file

@ -0,0 +1,16 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestEnv(t *testing.T) {
t.Run("True", func(t *testing.T) {
assert.True(t, Env(EnvTest))
})
t.Run("False", func(t *testing.T) {
assert.False(t, Env("foo"))
})
}

File diff suppressed because it is too large Load diff

View file

@ -74,6 +74,7 @@ type Options struct {
AppIcon string `yaml:"AppIcon" json:"AppIcon" flag:"app-icon"`
AppName string `yaml:"AppName" json:"AppName" flag:"app-name"`
AppMode string `yaml:"AppMode" json:"AppMode" flag:"app-mode"`
WallpaperUri string `yaml:"WallpaperUri" json:"WallpaperUri" flag:"wallpaper-uri"`
CdnUrl string `yaml:"CdnUrl" json:"CdnUrl" flag:"cdn-url"`
SiteUrl string `yaml:"SiteUrl" json:"SiteUrl" flag:"site-url"`
SiteAuthor string `yaml:"SiteAuthor" json:"SiteAuthor" flag:"site-author"`

View file

@ -200,6 +200,7 @@ func CliTestContext() *cli.Context {
globalSet.String("darktable-cli", config.DarktableBin, "doc")
globalSet.String("darktable-blacklist", config.DarktableBlacklist, "doc")
globalSet.String("wakeup-interval", "1h34m9s", "doc")
globalSet.Bool("test", true, "doc")
globalSet.Bool("debug", false, "doc")
globalSet.Bool("detect-nsfw", config.DetectNSFW, "doc")
globalSet.Int("auto-index", config.AutoIndex, "doc")
@ -224,6 +225,7 @@ func CliTestContext() *cli.Context {
LogError(c.Set("darktable-blacklist", "raf,cr3"))
LogError(c.Set("wakeup-interval", "1h34m9s"))
LogError(c.Set("detect-nsfw", "true"))
LogError(c.Set("test", "true"))
LogError(c.Set("auto-index", strconv.Itoa(config.AutoIndex)))
LogError(c.Set("auto-import", strconv.Itoa(config.AutoImport)))

View file

@ -19,7 +19,7 @@ func TestPhotosQueryPortrait(t *testing.T) {
t.Fatal(err)
}
assert.GreaterOrEqual(t, len(photos0), 40)
assert.GreaterOrEqual(t, len(photos0), 39)
t.Run("false > yes", func(t *testing.T) {
var f form.SearchPhotos

22
pkg/clean/uri.go Normal file
View file

@ -0,0 +1,22 @@
package clean
import (
"net/url"
"strings"
)
// Uri removes invalid character from an uri string.
func Uri(s string) string {
if s == "" || reject(s, 512) || strings.Contains(s, "..") {
return ""
}
// Trim whitespace.
s = strings.TrimSpace(s)
if uri, err := url.Parse(s); err != nil {
return ""
} else {
return uri.String()
}
}

18
pkg/clean/uri_test.go Normal file
View file

@ -0,0 +1,18 @@
package clean
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestUri(t *testing.T) {
t.Run("Valid", func(t *testing.T) {
result := Uri("https://docs.photoprism.app/getting-started/config-options/#file-converters")
assert.Equal(t, "https://docs.photoprism.app/getting-started/config-options/#file-converters", result)
})
t.Run("Invalid", func(t *testing.T) {
result := Uri("https://..docs.photoprism.app/gettin\\g-started/config-options/\tfile-converters")
assert.Equal(t, "", result)
})
}