Auth: Add support for config and routing extensions (WIP) #782 #2478

Enables developers to extend the existing config and API. Initial proof
of concept and work in progress. Implementation details may change.
Feedback welcome!

Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
Michael Mayer 2022-07-05 23:13:34 +02:00
parent 18473e4d44
commit e739dd3e89
38 changed files with 459 additions and 150 deletions

View file

@ -39,7 +39,9 @@ var log = event.Log
const appName = "PhotoPrism"
const appEdition = "PhotoPrism® CE"
const appDescription = "AI-Powered Photos App. Visit https://docs.photoprism.app/ to learn more."
const appDescription = "PhotoPrism® is an AI-Powered Photos App for the Decentralized Web." +
" It makes use of the latest technologies to tag and find pictures automatically without getting in your way." +
" You can run it at home, on a private server, or in the cloud."
const appCopyright = "(c) 2018-2022 PhotoPrism UG. All rights reserved."
// Metadata contains build specific information.

View file

@ -29,6 +29,7 @@ import themes from "options/themes.json";
import translations from "locales/translations.json";
import { Languages } from "options/options";
import { Photo } from "model/photo";
import { onSetTheme } from "common/hooks";
export default class Config {
/**
@ -340,13 +341,22 @@ export default class Config {
}
setTheme(name) {
this.themeName = name;
let theme = onSetTheme(name, this);
if (!theme) {
this.themeName = name;
theme = themes[name] ? themes[name] : themes["default"];
}
if (this.values.settings && this.values.settings.ui) {
this.values.settings.ui.theme = this.themeName;
}
Event.publish("view.refresh", this);
this.theme = themes[name] ? themes[name] : themes["default"];
this.theme = theme;
this.setBodyTheme(name);
this.setBodyTheme(this.themeName);
if (this.theme.dark) {
this.setColorMode("dark");

View file

@ -0,0 +1,3 @@
export function onSetTheme(/*name, config*/) {
return false;
}

View file

@ -4,14 +4,14 @@
<v-layout wrap align-top pt-3>
<v-flex xs12 sm6 class="px-0 pb-2 body-1 text-selectable text-xs-left">
<strong><router-link to="/about" class="text-link">{{ $config.getEdition() }}</router-link></strong>
<span class="body-link text-selectable">Build <a href="https://docs.photoprism.app/release-notes/" target="_blank" :title="version">{{ build }}</a></span>
<span class="body-link text-selectable">Build <a href="https://docs.photoprism.app/release-notes/" target="_blank" :title="version" class="body-link">{{ build }}</a></span>
</v-flex>
<v-flex xs12 sm6 class="px-0 pb-2 body-1 text-xs-center text-sm-right">
<div class="hidden-xs-only">
<a href="https://raw.githubusercontent.com/photoprism/photoprism/develop/NOTICE"
target="_blank" class="text-link">3rd-party software packages</a>
<a href="https://photoprism.app/team/" target="_blank">© 2018-2022 PhotoPrism UG</a>
<a href="https://photoprism.app/team/" target="_blank" class="body-link">© 2018-2022 PhotoPrism UG</a>
</div>
</v-flex>
</v-layout>

View file

@ -13,11 +13,15 @@
width: 190px;
height: 30vh;
max-height: 190px;
margin: 32px auto 16px auto;
margin: 20px auto 40px auto;
position: relative;
left: unset;
}
#photoprism .auth-login .action-buttons {
margin: 30px auto 20px auto;
}
#photoprism main .auth-login .logo img {
height: 100%;
}

View file

@ -214,7 +214,7 @@ export const Languages = () => [
},
];
export const Themes = () => [
let themes = [
{
text: $gettext("Default"),
value: "default",
@ -281,6 +281,12 @@ export const Themes = () => [
disabled: false,
},
];
export const Themes = () => themes;
export const SetThemes = (v) => {
themes = v;
};
export const MapsAnimate = () => [
{
text: $gettext("None"),

View file

@ -12,42 +12,62 @@
</v-btn>
</v-toolbar>
<v-container fluid class="px-4 pt-4 pb-1">
<p class="body-2 lh-17">
<translate>Our mission is to provide the most user- and privacy-friendly solution to keep your pictures organized and accessible.</translate>
<a href="https://link.photoprism.app/roadmap" target="_blank">
<translate>The roadmap shows what tasks are in progress, what needs testing, and which feature requests are going to be implemented next.</translate>
<p class="body-2">
<translate>PhotoPrism® is an AI-Powered Photos App for the Decentralized Web.</translate>
<translate>It makes use of the latest technologies to tag and find pictures automatically without getting in your way.</translate>
<translate>You can run it at home, on a private server, or in the cloud.</translate>
</p>
<p class="body-1 pb-1">
<span v-if="sponsor">
<translate>Your continued support helps us provide regular updates and remain independent, so we can fulfill our mission and protect your privacy.</translate>
</span>
<span v-else>
<translate>Sponsors get access to additional features, receive direct technical support via email, and can join our private chat room on matrix.org.</translate>
</span>
<translate>Being 100% self-funded and independent, we can promise you that we will never sell your data and that we will always be transparent about our software and services.</translate>
</p>
<div v-if="!sponsor">
<p class="text-xs-center my-4">
<v-btn
href="https://photoprism.app/membership"
target="_blank"
color="primary-button"
class="white--text px-3 py-2 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>
</p>
<p class="body-1 pt-2">
<a target="_blank" href="https://link.photoprism.app/github">
<translate>Also, please leave a star on GitHub if you like this project. It provides additional motivation to keep going.</translate>
</a>
</p>
</p>
</div>
<p v-if="!sponsor" class="text-xs-center my-4">
<v-btn
href="https://photoprism.app/membership"
target="_blank"
color="primary-button"
class="white--text px-3 py-2 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>
<h3 class="subheading py-2">User Guide</h3>
<p>
<translate>Visit docs.photoprism.app/user-guide to learn how to sync, organize, and share your pictures.</translate>
<translate>Our User Guide also covers many advanced topics, such as migrating from Google Photos and thumbnail quality settings.</translate>
<translate>Common issues can be quickly diagnosed and solved using the troubleshooting checklists we provide.</translate>
</p>
<p><a href="https://docs.photoprism.app/user-guide/" class="text-link" target="_blank">Read the docs </a></p>
<p class="body-1">
<translate>Your continued support helps us provide regular updates and services like world maps.</translate>
<translate>Sponsors get access to additional features, receive direct technical support via email, and can join our private chat room on matrix.org.</translate>
</p>
<p v-if="!sponsor" class="body-1 pb-2">
<a target="_blank" href="https://link.photoprism.app/github">
<translate>Also, please leave a star on GitHub if you like this project. It provides additional motivation to keep going.</translate>
</a>
</p>
<h2 class="mt-4 mb-2 body-2 lh-17">
<h3 class="subheading py-2">Knowledge Base</h3>
<p>Browse the Knowledge Base for detailed information on specific product features, services, and related resources.</p>
<p><a href="https://photoprism.app/kb" class="text-link" target="_blank">Learn more </a></p>
<h3 class="subheading py-2">
<translate>Getting Support</translate>
</h2>
</h3>
<p class="body-1">
<a target="_blank" href="https://docs.photoprism.app/getting-started/troubleshooting/">
<translate>Before submitting a support request, please use our Troubleshooting Checklists to determine the cause of your problem.</translate>
<translate>If this doesn't help, or you have other questions:</translate>
<translate>Before submitting a support request, please use our Troubleshooting Checklists to determine the cause of your problem.</translate>
<translate>If this doesn't help, or you have other questions:</translate>
</a>
</p>
<ul class="body-1 mb-3">
@ -55,12 +75,11 @@
<li><a target="_blank" href="https://link.photoprism.app/discussions"><translate>post your question in GitHub Discussions</translate></a></li>
<li><a target="_blank" href="https://link.photoprism.app/chat"><translate>or ask in our Community Chat</translate></a></li>
</ul>
<p class="body-1">
<p class="body-1 pb-2">
<a target="_blank" href="https://photoprism.app/contact"><translate>In addition, sponsors receive direct technical support via email.</translate></a>
</p>
<p v-if="!sponsor" class="body-1 pb-2">
<translate>We'll do our best to answer all your questions. In return, we ask you to back us on Patreon or GitHub Sponsors.</translate>
<span v-if="!sponsor">
<translate>We'll do our best to answer all your questions. In return, we ask you to back us on Patreon or GitHub Sponsors.</translate>
</span>
</p>
<p v-if="sponsor" class="text-xs-center">

View file

@ -1,10 +1,7 @@
<template>
<div class="p-page p-page-support">
<v-toolbar flat color="secondary" :dense="$vuetify.breakpoint.smAndDown">
<v-toolbar-title v-if="sent">
<translate>Your message has been sent</translate>
</v-toolbar-title>
<v-toolbar-title v-else>
<v-toolbar-title>
<translate>Contact Us</translate>
</v-toolbar-title>
<v-spacer></v-spacer>
@ -13,7 +10,16 @@
</v-btn>
</v-toolbar>
<v-container v-if="sent" fluid class="pa-4">
<p class="body-1">We'll get back to you as soon as possible!</p>
<h3 class="title font-weight-bold pt-4 pb-2 text-xs-center">
<translate>We appreciate your feedback!</translate>
</h3>
<p class="body-2 py-4 text-xs-center">
<translate>Due to the high volume of emails we receive, our team may be unable to get back to you immediately.</translate>
<translate>We do our best to respond within five business days or less.</translate>
</p>
<p class="mt-4 text-xs-center">
<img src="https://cdn.photoprism.app/thank-you/colorful.png" width="100%" alt="THANK YOU">
</p>
</v-container>
<v-form v-else ref="form" v-model="valid"
autocomplete="off"

View file

@ -4,7 +4,7 @@
<v-flex xs12 sm8 md4 xl3 xxl2>
<v-form ref="form" dense class="auth-login-form" accept-charset="UTF-8" @submit.prevent="login">
<v-card class="elevation-12 auth-login-box blur-7">
<v-card-text class="pa-3">
<v-card-text class="pa-4">
<div class="logo text-xs-center">
<img :src="$config.getIcon()" :alt="config.name">
</div>
@ -43,10 +43,10 @@
@keyup.enter.native="login"
></v-text-field>
<v-spacer></v-spacer>
<div class="pa-3 text-xs-center">
<div class="action-buttons text-xs-center">
<!-- a href="#" target="_blank" class="text-link px-2" :style="`color: ${colors.link}!important`"><translate>Forgot password?</translate></a -->
<v-btn :color="colors.primary" round :disabled="loginDisabled"
class="white--text action-confirm px-3" @click.stop="login">
<v-btn :color="colors.primary" depressed :disabled="loginDisabled"
class="white--text action-confirm px-4" @click.stop="login">
<translate>Sign in</translate>
<v-icon :right="!rtl" :left="rtl" dark>arrow_forward</v-icon>
</v-btn>
@ -58,15 +58,18 @@
</v-layout>
<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.siteCaption ? config.siteCaption : config.siteTitle }}</strong>
<v-flex xs12 class="pa-0 body-2 text-selectable text-xs-center white--tex text-sm-left sm6">
{{ $config.getEdition() }}
</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>
<span v-else>{{ config.imprint }}</span>
</v-flex>
<v-flex v-else xs12 class="pa-0 body-2 text-selectable text-xs-center white--text text-sm-right sm6">
<strong>{{ config.siteCaption ? config.siteCaption : config.siteTitle }}</strong>
</v-flex>
</v-layout>
</footer>
<footer v-else>

10
go.mod
View file

@ -15,7 +15,7 @@ require (
github.com/dsoprea/go-utility v0.0.0-20200717064901-2fccff4aa15e // indirect
github.com/dustin/go-humanize v1.0.0
github.com/esimov/pigo v1.4.5
github.com/gin-contrib/gzip v0.0.5
github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.8.1
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-playground/validator/v10 v10.11.0 // indirect
@ -43,7 +43,7 @@ require (
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
github.com/sevlyar/go-daemon v0.1.5
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.5
github.com/stretchr/testify v1.8.0
github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f
github.com/tensorflow/tensorflow v1.15.2
github.com/tidwall/gjson v1.14.1
@ -51,7 +51,7 @@ require (
github.com/urfave/cli v1.22.9
go4.org v0.0.0-20201209231011-d4a079459e60 // indirect
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e
golang.org/x/net v0.0.0-20220630215102-69896b714898
gonum.org/v1/gonum v0.11.0
gopkg.in/photoprism/go-tz.v2 v2.1.1
gopkg.in/yaml.v2 v2.4.0
@ -70,7 +70,7 @@ require (
require (
github.com/google/uuid v1.3.0
github.com/pelletier/go-toml/v2 v2.0.2 // indirect
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c // indirect
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e // indirect
)
require (
@ -82,7 +82,7 @@ require (
github.com/go-playground/locales v0.14.0 // indirect
github.com/go-playground/universal-translator v0.18.0 // indirect
github.com/go-sql-driver/mysql v1.5.0 // indirect
github.com/goccy/go-json v0.9.7 // indirect
github.com/goccy/go-json v0.9.8 // indirect
github.com/gosimple/unidecode v1.0.1 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/leodido/go-urn v1.2.1 // indirect

31
go.sum
View file

@ -92,11 +92,10 @@ github.com/esimov/pigo v1.4.5 h1:ySG0QqMh02VNALvHnx04L1ScRu66N6XA5vLLga8GiLg=
github.com/esimov/pigo v1.4.5/go.mod h1:SGkOUpm4wlEmQQJKlaymAkThY8/8iP+XE0gFo7g8G6w=
github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
github.com/gin-contrib/gzip v0.0.5 h1:mhnVU32YnnBh2LPH2iqRqsA/eR7SAqRaD388jL2s/j0=
github.com/gin-contrib/gzip v0.0.5/go.mod h1:OPIK6HR0Um2vNmBUTlayD7qle4yVVRZT0PyhdUigrKk=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.7.4/go.mod h1:jD2toBW3GZUr5UMcdrwQA10I7RuaFOl/SGeDjXkfUtY=
github.com/gin-gonic/gin v1.8.1 h1:4+fr/el88TOO3ewCmQr8cx/CtZ/umlIRIs5M4NTNjf8=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q=
@ -117,13 +116,10 @@ github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhO
github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M=
github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
github.com/go-playground/locales v0.14.0 h1:u50s323jtVGugKlcYeyzC0etD1HifMjqmJqb8WugfUU=
github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
github.com/go-playground/universal-translator v0.18.0 h1:82dyy6p4OuJq4/CByFNOn/jYrnRPArHwAcmLoJZxyho=
github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.11.0 h1:0W+xRM511GY47Yy3bZUbJVitCNg2BOGlCyvTqsp/xIw=
github.com/go-playground/validator/v10 v10.11.0/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4dMGDBiPU55YFDl0WbKdWU=
@ -132,8 +128,9 @@ github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB
github.com/go-xmlfmt/xmlfmt v0.0.0-20191208150333-d5b6f63a941b/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/go-xmlfmt/xmlfmt v0.0.0-20220206211657-0a94163c4677 h1:+k/R5MXzpgWkdqHjiuirfHk6QzzTToFxlKVrvkSR/ek=
github.com/go-xmlfmt/xmlfmt v0.0.0-20220206211657-0a94163c4677/go.mod h1:aUCEOzzezBEjDBbFBoSiya/gduyIiWYRP6CnSFIV8AM=
github.com/goccy/go-json v0.9.7 h1:IcB+Aqpx/iMHu5Yooh7jEzJk1JZ7Pjtmys2ukPr7EeM=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/goccy/go-json v0.9.8 h1:DxXB6MLd6yyel7CLph8EwNIonUtVZd3Ue5iRcL4DQCE=
github.com/goccy/go-json v0.9.8/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
@ -194,7 +191,6 @@ github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M=
github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -218,7 +214,6 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leandro-lugaresi/hub v1.1.1 h1:zqp0HzFvj4HtqjMBXM2QF17o6PNmR8MJOChgeKl/aw8=
github.com/leandro-lugaresi/hub v1.1.1/go.mod h1:XEFWanhHv6Rt3XlteHMxuNDYi8dJcpJjodpqkU+BtIo=
github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII=
github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leonelquinteros/gotext v1.5.0 h1:ODY7LzLpZWWSJdAHnzhreOr6cwLXTAmc914FOauSkBM=
@ -236,7 +231,6 @@ github.com/mandykoh/prism v0.35.0/go.mod h1:8l+gpXl2w4aHUtgp9SEv3PFDb0OsrwMyEJUP
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@ -249,7 +243,6 @@ github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/montanaflynn/stats v0.6.6 h1:Duep6KMIDpY4Yo11iFsvyqJDyfzLF9+sndUKT+v64GQ=
@ -303,8 +296,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
github.com/stretchr/testify v1.7.5 h1:s5PTfem8p8EbKQOctVV53k6jCJt3UX4IEJzwh+C324Q=
github.com/stretchr/testify v1.7.5/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f h1:SLJx0nHhb2ZLlYNMAbrYsjwmVwXx4yRT48lNIxOp7ts=
github.com/studio-b12/gowebdav v0.0.0-20211106090535-29e74efa701f/go.mod h1:gCcfDlA1Y7GqOaeEKw5l9dOGx1VLdc/HuQSlQAaZ30s=
github.com/tensorflow/tensorflow v1.15.2 h1:7/f/A664Tml/nRJg04+p3StcrsT53mkcvmxYHXI21Qo=
@ -315,10 +308,8 @@ github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw=
github.com/ugorji/go v1.2.7 h1:qYhyWUUd6WbiM+C6JZAUkIJt/1WrjzNHY9+KCIjVqTo=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0=
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
github.com/ulule/deepcopier v0.0.0-20200430083143-45decc6639b6 h1:TtyC78WMafNW8QFfv3TeP3yWNDG+uxNkk9vOrnDu6JA=
@ -417,8 +408,8 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e h1:TsQ7F31D3bUCLeqPT0u+yjp1guoArKaNKmCr22PYgTQ=
golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -445,7 +436,6 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -461,8 +451,8 @@ golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c h1:aFV+BgZ4svzjfabn8ERpuB4JI4N6/rdy1iusx77G3oU=
golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e h1:CsOuNlbOuf0mzxJIefr6Q4uAUetRUwZE4qt7VfzP+xo=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20191110171634-ad39bd3f0407/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@ -566,7 +556,6 @@ gopkg.in/photoprism/go-tz.v2 v2.1.1 h1:XdNAQRneJmJdXDFovXJbf5eewp3zsir+jJ1Bxdmbn
gopkg.in/photoprism/go-tz.v2 v2.1.1/go.mod h1:E1aQvLJs3YA4wbrPMOdX4YEx1TgRO2PLSxnO+J1Kqiw=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

View file

@ -31,7 +31,6 @@ 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

@ -2,6 +2,7 @@ package config
import (
"reflect"
"strings"
"github.com/photoprism/photoprism/pkg/list"
"github.com/urfave/cli"
@ -29,6 +30,11 @@ func (f CliFlag) Fields() reflect.Value {
return fields
}
// Default returns the default value.
func (f CliFlag) Default() string {
return f.Flag.GetValue()
}
// Hidden checks if the flag is hidden.
func (f CliFlag) Hidden() bool {
field := f.Fields().FieldByName("Hidden")
@ -56,6 +62,12 @@ func (f CliFlag) Name() string {
return f.Flag.GetName()
}
// CommandFlag returns the full command flag based on the name.
func (f CliFlag) CommandFlag() string {
n := strings.Split(f.Name(), ",")
return "--" + n[0]
}
// Usage returns the command flag usage.
func (f CliFlag) Usage() string {
if list.Contains(f.Tags, EnvSponsor) {

View file

@ -1,8 +1,9 @@
package config
import (
"github.com/photoprism/photoprism/pkg/list"
"github.com/urfave/cli"
"github.com/photoprism/photoprism/pkg/list"
)
// CliFlags represents a list of command-line parameters.
@ -10,14 +11,13 @@ type CliFlags []CliFlag
// Cli returns the currently active command-line parameters.
func (f CliFlags) Cli() (result []cli.Flag) {
var tags []string
result = make([]cli.Flag, 0, len(f))
switch {
case Sponsor():
tags = []string{EnvSponsor}
for _, flag := range f {
result = append(result, flag.Flag)
}
return f.Find(tags)
return result
}
// Find finds command-line parameters based on a list of tags.
@ -34,3 +34,49 @@ func (f CliFlags) Find(tags []string) (result []cli.Flag) {
return result
}
// Remove removes command flags by name.
func (f CliFlags) Remove(names []string) (result CliFlags) {
result = make(CliFlags, 0, len(f))
for _, flag := range f {
if list.Contains(names, flag.Name()) {
continue
}
result = append(result, flag)
}
return result
}
// Insert inserts command flags, if possible after name.
func (f CliFlags) Insert(name string, insert []CliFlag) (result CliFlags) {
result = make(CliFlags, 0, len(f)+len(insert))
done := false
for _, flag := range f {
result = append(result, flag)
if !done && flag.Name() == name {
result = append(result, insert...)
done = true
}
}
if !done {
result = append(result, insert...)
}
return result
}
// Prepend adds command flags at the beginning.
func (f CliFlags) Prepend(el []CliFlag) (result CliFlags) {
result = make(CliFlags, 0, len(f)+len(el))
result = append(result, el...)
return append(result, f...)
}

View file

@ -2,7 +2,7 @@ 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"}
cols = []string{"Environment", "CLI Flag", "Default", "Description"}
rows = make([][]string, 0, len(f))
@ -11,8 +11,7 @@ func (f CliFlags) Report() (rows [][]string, cols []string) {
continue
}
row := []string{flag.EnvVar(), flag.Name(), flag.Usage()}
rows = append(rows, row)
rows = append(rows, []string{flag.EnvVar(), flag.CommandFlag(), flag.Default(), flag.Usage()})
}
return rows, cols

View file

@ -12,6 +12,14 @@ import (
"github.com/photoprism/photoprism/pkg/txt"
)
type ClientType string
const (
ClientPublic ClientType = "public"
ClientGuest ClientType = "guest"
ClientUser ClientType = "user"
)
// ClientConfig represents HTTP client / Web UI config options.
type ClientConfig struct {
Mode string `json:"mode"`
@ -69,6 +77,7 @@ type ClientConfig struct {
Categories CategoryLabels `json:"categories"`
Clip int `json:"clip"`
Server env.Resources `json:"server"`
Ext Values `json:"ext"`
}
// Years represents a list of years.
@ -249,6 +258,7 @@ func (c *Config) PublicConfig() ClientConfig {
Clip: txt.ClipDefault,
PreviewToken: "public",
DownloadToken: "public",
Ext: ClientExt(c, ClientPublic),
}
return result
@ -325,6 +335,7 @@ func (c *Config) GuestConfig() ClientConfig {
PreviewToken: c.PreviewToken(),
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
Ext: ClientExt(c, ClientGuest),
}
return result
@ -395,6 +406,7 @@ func (c *Config) UserConfig() ClientConfig {
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
Server: env.Info(),
Ext: ClientExt(c, ClientUser),
}
c.Db().

View file

@ -0,0 +1,13 @@
package config
// ClientExt returns optional client config values by namespace.
func ClientExt(c *Config, t ClientType) Values {
configs := Extensions()
result := make(Values, len(configs))
for _, conf := range configs {
result[conf.name] = conf.clientValues(c, t)
}
return result
}

View file

@ -109,6 +109,15 @@ func NewConfig(ctx *cli.Context) *Config {
}
}
// Initialize package extensions.
for _, ext := range Extensions() {
if err := ext.init(c); err != nil {
log.Warnf("config: failed to initialize extension %s (%s)", clean.Log(ext.name), err)
} else {
log.Debugf("config: extension %s initialized", clean.Log(ext.name))
}
}
return c
}
@ -409,7 +418,7 @@ func (c *Config) SiteDescription() string {
// SitePreview returns the site preview image URL for sharing.
func (c *Config) SitePreview() string {
if c.options.SitePreview == "" {
if c.options.SitePreview == "" || c.NoSponsor() {
return c.SiteUrl() + "static/img/preview.jpg"
}
@ -462,9 +471,9 @@ func (c *Config) Demo() bool {
return c.options.Demo
}
// Sponsor reports if your continuous support helps to pay for development and operating expenses.
// Sponsor reports if you support our mission, see https://photoprism.app/membership.
func (c *Config) Sponsor() bool {
return c.options.Sponsor || c.Test()
return Sponsor || c.options.Sponsor
}
// NoSponsor reports if the instance is not operated by a sponsor.
@ -515,9 +524,14 @@ func (c *Config) AdminPassword() string {
return c.options.AdminPassword
}
// Auth checks if authentication is always required.
// AuthMode returns the authentication mode.
func (c *Config) AuthMode() string {
return strings.ToLower(strings.TrimSpace(c.options.AuthMode))
}
// Auth checks if authentication is required.
func (c *Config) Auth() bool {
return c.options.Auth
return c.AuthMode() != ""
}
// LogLevel returns the Logrus log level.
@ -673,6 +687,10 @@ func (c *Config) OriginalsLimitBytes() int64 {
// ResolutionLimit returns the maximum resolution of originals in megapixels (width x height).
func (c *Config) ResolutionLimit() int {
if c.NoSponsor() {
return 100
}
result := c.options.ResolutionLimit
if result <= 0 {

View file

@ -62,16 +62,14 @@ func (c *Config) AppName() string {
name = c.SiteTitle()
}
clean := func(r rune) rune {
name = strings.Map(func(r rune) rune {
switch r {
case '\'', '"':
return -1
}
return r
}
name = strings.Map(clean, name)
}, name)
return txt.Clip(name, 32)
}
@ -109,7 +107,7 @@ func (c *Config) WallpaperUri() string {
// Valid URI? Local file?
if p := clean.Path(c.options.WallpaperUri); p == "" {
return ""
c.options.WallpaperUri = ""
} else if fs.FileExists(filepath.Join(c.StaticPath(), assetPath, p)) {
c.options.WallpaperUri = path.Join(c.StaticUri(), assetPath, p)
} else {

View file

@ -20,7 +20,7 @@ func TestConfig_DefaultTheme(t *testing.T) {
assert.Equal(t, "grayscale", c.DefaultTheme())
c.options.Sponsor = false
c.options.Test = true
assert.Equal(t, "grayscale", c.DefaultTheme())
assert.Equal(t, "default", c.DefaultTheme())
c.options.Sponsor = false
c.options.Test = false
assert.Equal(t, "default", c.DefaultTheme())
@ -96,9 +96,15 @@ func TestConfig_WallpaperUri(t *testing.T) {
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())
assert.Equal(t, "https://cdn.photoprism.app/wallpaper/welcome.jpg", c.WallpaperUri())
c.options.Test = true
assert.Equal(t, "https://cdn.photoprism.app/wallpaper/welcome.jpg", c.WallpaperUri())
c.options.Sponsor = false
assert.Equal(t, "", c.WallpaperUri())
c.options.Sponsor = true
assert.Equal(t, "https://cdn.photoprism.app/wallpaper/welcome.jpg", c.WallpaperUri())
c.options.WallpaperUri = "kashmir"
assert.Equal(t, "/static/img/wallpaper/kashmir.jpg", c.WallpaperUri())
c.options.WallpaperUri = ""
assert.Equal(t, "", c.WallpaperUri())
}

View file

@ -0,0 +1,34 @@
package config
import (
"sync"
"sync/atomic"
)
var (
extMutex sync.Mutex
extensions atomic.Value
)
// Extension represents a named package extension with callbacks.
type Extension struct {
name string
init func(c *Config) error
clientValues func(c *Config, t ClientType) Values
}
// Register registers a new package extension.
func Register(name string, initConfig func(c *Config) error, clientConfig func(c *Config, t ClientType) Values) {
extMutex.Lock()
n, _ := extensions.Load().([]Extension)
extensions.Store(append(n, Extension{name, initConfig, clientConfig}))
extMutex.Unlock()
}
// Extensions returns all registered package extensions.
func Extensions() (ext []Extension) {
extMutex.Lock()
ext, _ = extensions.Load().([]Extension)
extMutex.Unlock()
return ext
}

View file

@ -31,7 +31,7 @@ func (c *Config) FaceOverlap() int {
// FaceClusterSize returns the size threshold for faces forming a cluster in pixels.
func (c *Config) FaceClusterSize() int {
if c.options.FaceClusterSize < 20 || c.options.FaceClusterSize > 10000 {
if c.NoSponsor() || c.options.FaceClusterSize < 20 || c.options.FaceClusterSize > 10000 {
return face.ClusterSizeThreshold
}
@ -40,7 +40,7 @@ func (c *Config) FaceClusterSize() int {
// FaceClusterScore returns the quality threshold for faces forming a cluster.
func (c *Config) FaceClusterScore() int {
if c.options.FaceClusterScore < 1 || c.options.FaceClusterScore > 100 {
if c.NoSponsor() || c.options.FaceClusterScore < 1 || c.options.FaceClusterScore > 100 {
return face.ClusterScoreThreshold
}
@ -49,7 +49,7 @@ func (c *Config) FaceClusterScore() int {
// FaceClusterCore returns the number of faces forming a cluster core.
func (c *Config) FaceClusterCore() int {
if c.options.FaceClusterCore < 1 || c.options.FaceClusterCore > 100 {
if c.NoSponsor() || c.options.FaceClusterCore < 1 || c.options.FaceClusterCore > 100 {
return face.ClusterCore
}
@ -58,7 +58,7 @@ func (c *Config) FaceClusterCore() int {
// FaceClusterDist returns the radius of faces forming a cluster core.
func (c *Config) FaceClusterDist() float64 {
if c.options.FaceClusterDist < 0.1 || c.options.FaceClusterDist > 1.5 {
if c.NoSponsor() || c.options.FaceClusterDist < 0.1 || c.options.FaceClusterDist > 1.5 {
return face.ClusterDist
}
@ -67,7 +67,7 @@ func (c *Config) FaceClusterDist() float64 {
// FaceMatchDist returns the offset distance when matching faces with clusters.
func (c *Config) FaceMatchDist() float64 {
if c.options.FaceMatchDist < 0.1 || c.options.FaceMatchDist > 1.5 {
if c.NoSponsor() || c.options.FaceMatchDist < 0.1 || c.options.FaceMatchDist > 1.5 {
return face.MatchDist
}

View file

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

View file

@ -14,6 +14,13 @@ func (c *Config) FFmpegEnabled() bool {
// FFmpegEncoder returns the FFmpeg AVC encoder name.
func (c *Config) FFmpegEncoder() ffmpeg.AvcEncoder {
if c.options.FFmpegEncoder == "" || c.options.FFmpegEncoder == ffmpeg.SoftwareEncoder.String() {
return ffmpeg.SoftwareEncoder
} else if c.NoSponsor() {
log.Infof("ffmpeg: hardware transcoding is available to sponsors only")
return ffmpeg.SoftwareEncoder
}
return ffmpeg.FindEncoder(c.options.FFmpegEncoder)
}

View file

@ -362,6 +362,15 @@ func (c *Config) AssetsPath() string {
return fs.Abs(c.options.AssetsPath)
}
// CustomAssetsPath returns the path to custom assets such as icons, models and translations.
func (c *Config) CustomAssetsPath() string {
if c.options.CustomAssetsPath != "" {
return fs.Abs(c.options.CustomAssetsPath)
}
return ""
}
// LocalesPath returns the translation locales path.
func (c *Config) LocalesPath() string {
return filepath.Join(c.AssetsPath(), "locales")

View file

@ -167,5 +167,9 @@ func (c *Config) Report() (rows [][]string, cols []string) {
{"log-filename", c.LogFilename()},
}
if p := c.CustomAssetsPath(); p != "" {
rows = append(rows, []string{"custom-assets-path", p})
}
return rows, cols
}

View file

@ -2,6 +2,7 @@ package config
import (
"path/filepath"
"regexp"
"strings"
"github.com/photoprism/photoprism/pkg/fs"
@ -53,9 +54,45 @@ func (c *Config) TemplatesPath() string {
return filepath.Join(c.AssetsPath(), "templates")
}
// CustomTemplatesPath returns the path to custom templates.
func (c *Config) CustomTemplatesPath() string {
if p := c.CustomAssetsPath(); p != "" {
return filepath.Join(p, "templates")
}
return ""
}
// TemplateFiles returns the file paths of all templates found.
func (c *Config) TemplateFiles() []string {
results := make([]string, 0, 32)
tmplPaths := []string{c.TemplatesPath(), c.CustomTemplatesPath()}
for _, p := range tmplPaths {
matches, err := filepath.Glob(regexp.QuoteMeta(p) + "/[A-Za-z0-9]*.*")
if err != nil {
continue
}
for _, tmplName := range matches {
results = append(results, tmplName)
}
}
return results
}
// TemplateExists checks if a template with the given name exists (e.g. index.tmpl).
func (c *Config) TemplateExists(name string) bool {
return fs.FileExists(filepath.Join(c.TemplatesPath(), name))
if found := fs.FileExists(filepath.Join(c.TemplatesPath(), name)); found {
return true
} else if p := c.CustomTemplatesPath(); p != "" {
return fs.FileExists(filepath.Join(p, name))
} else {
return false
}
}
// TemplateName returns the name of the default template (e.g. index.tmpl).

View file

@ -177,6 +177,12 @@ func TestConfig_AssetsPath(t *testing.T) {
assert.True(t, strings.HasSuffix(c.AssetsPath(), "/assets"))
}
func TestConfig_CustomAssetsPath(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "", c.CustomAssetsPath())
}
func TestConfig_DetectNSFW(t *testing.T) {
c := NewConfig(CliTestContext())
@ -224,6 +230,21 @@ func TestConfig_TemplatesPath(t *testing.T) {
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/templates", path)
}
func TestConfig_CustomTemplatesPath(t *testing.T) {
c := NewConfig(CliTestContext())
path := c.CustomTemplatesPath()
assert.Equal(t, "", path)
}
func TestConfig_TemplatesFiles(t *testing.T) {
c := NewConfig(CliTestContext())
files := c.TemplateFiles()
t.Logf("TemplateFiles: %#v", files)
}
func TestConfig_StaticPath(t *testing.T) {
c := NewConfig(CliTestContext())
@ -357,6 +378,10 @@ func TestConfig_ResolutionLimit(t *testing.T) {
assert.Equal(t, -1, c.ResolutionLimit())
c.options.ResolutionLimit = -1
assert.Equal(t, -1, c.ResolutionLimit())
c.options.Sponsor = false
assert.Equal(t, 100, c.ResolutionLimit())
c.options.Sponsor = true
assert.Equal(t, -1, c.ResolutionLimit())
}
func TestConfig_BaseUri(t *testing.T) {

View file

@ -19,19 +19,12 @@ var Flags = CliFlags{
Name: "admin-password, pw",
Usage: fmt.Sprintf("initial admin `PASSWORD`, must have at least %d characters", entity.PasswordLength),
EnvVar: "PHOTOPRISM_ADMIN_PASSWORD",
},
},
CliFlag{
Flag: cli.BoolFlag{
Name: "public, p",
Usage: "disable password authentication, WebDAV, and the advanced settings page",
EnvVar: "PHOTOPRISM_PUBLIC",
}},
CliFlag{
Flag: cli.BoolFlag{
Name: "auth, a",
Usage: "always require password authentication, overrides the public flag if it is set",
EnvVar: "PHOTOPRISM_AUTH",
Name: "public",
Usage: "disable password authentication, incl WebDAV and Advanced Settings",
EnvVar: "PHOTOPRISM_PUBLIC",
}},
CliFlag{
Flag: cli.StringFlag{
@ -39,8 +32,7 @@ var Flags = CliFlags{
Usage: "log message verbosity `LEVEL` (trace, debug, info, warning, error, fatal, panic)",
Value: "info",
EnvVar: "PHOTOPRISM_LOG_LEVEL",
},
},
}},
CliFlag{
Flag: cli.BoolFlag{
Name: "debug",
@ -416,7 +408,8 @@ var Flags = CliFlags{
Usage: "site `CAPTION`",
Value: "AI-Powered Photos App",
EnvVar: "PHOTOPRISM_SITE_CAPTION",
}},
},
},
CliFlag{
Flag: cli.StringFlag{
Name: "site-description",

View file

@ -28,7 +28,7 @@ type Options struct {
LogLevel string `yaml:"LogLevel" json:"-" flag:"log-level"`
Debug bool `yaml:"Debug" json:"Debug" flag:"debug"`
Trace bool `yaml:"Trace" json:"Trace" flag:"Trace"`
Auth bool `yaml:"Auth" json:"-" flag:"auth"`
AuthMode string `yaml:"AuthMode" json:"-" flag:"auth-mode"`
Public bool `yaml:"Public" json:"-" flag:"public"`
Test bool `yaml:"-" json:"Test,omitempty" flag:"test"`
Unsafe bool `yaml:"-" json:"-" flag:"unsafe"`
@ -47,6 +47,7 @@ type Options struct {
CachePath string `yaml:"CachePath" json:"-" flag:"cache-path"`
ImportPath string `yaml:"ImportPath" json:"-" flag:"import-path"`
AssetsPath string `yaml:"AssetsPath" json:"-" flag:"assets-path"`
CustomAssetsPath string `yaml:"-" json:"-" flag:"custom-assets-path"`
TempPath string `yaml:"TempPath" json:"-" flag:"temp-path"`
Workers int `yaml:"Workers" json:"Workers" flag:"workers"`
WakeupInterval time.Duration `yaml:"WakeupInterval" json:"WakeupInterval" flag:"wakeup-interval"`

View file

@ -45,7 +45,7 @@ var PkgNameRegexp = regexp.MustCompile("[^a-zA-Z\\-_]+")
func NewTestOptions(pkg string) *Options {
assetsPath := fs.Abs("../../assets")
storagePath := fs.Abs("../../storage")
testDataPath := filepath.Join(storagePath, "testdata")
dataPath := filepath.Join(storagePath, "testdata")
pkg = PkgNameRegexp.ReplaceAllString(pkg, "")
driver := os.Getenv("PHOTOPRISM_TEST_DRIVER")
@ -79,7 +79,8 @@ func NewTestOptions(pkg string) *Options {
Version: "0.0.0",
Copyright: "(c) 2018-2022 PhotoPrism UG. All rights reserved.",
Public: true,
Auth: false,
Sponsor: true,
AuthMode: "",
Test: true,
Debug: true,
Trace: false,
@ -91,13 +92,13 @@ func NewTestOptions(pkg string) *Options {
AssetsPath: assetsPath,
AutoIndex: -1,
AutoImport: 7200,
StoragePath: testDataPath,
CachePath: testDataPath + "/cache",
OriginalsPath: testDataPath + "/originals",
ImportPath: testDataPath + "/import",
TempPath: testDataPath + "/temp",
ConfigPath: testDataPath + "/config",
SidecarPath: testDataPath + "/sidecar",
StoragePath: dataPath,
CachePath: dataPath + "/cache",
OriginalsPath: dataPath + "/originals",
ImportPath: dataPath + "/import",
TempPath: dataPath + "/temp",
ConfigPath: dataPath + "/config",
SidecarPath: dataPath + "/sidecar",
DatabaseDriver: driver,
DatabaseDsn: dsn,
AdminPassword: "photoprism",
@ -200,9 +201,10 @@ 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.Bool("debug", false, "doc")
globalSet.Bool("sponsor", true, "doc")
globalSet.Bool("test", true, "doc")
globalSet.Int("auto-index", config.AutoIndex, "doc")
globalSet.Int("auto-import", config.AutoImport, "doc")
@ -225,6 +227,8 @@ 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("debug", "false"))
LogError(c.Set("sponsor", "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

@ -0,0 +1,4 @@
package config
// Values is a shortcut for map[string]interface{}
type Values map[string]interface{}

View file

@ -31,7 +31,7 @@ func Logger() gin.HandlerFunc {
}
// Use debug level to keep production logs clean.
log.Debugf("http: %s %s (%3d) [%v]",
log.Debugf("server: %s %s (%3d) [%v]",
method,
clean.Log(path),
statusCode,

View file

@ -25,7 +25,7 @@ func Recovery() gin.HandlerFunc {
if err := recover(); err != nil {
stack := stack(3)
req, _ := httputil.DumpRequest(c.Request, false)
log.Debugf("http: %s (%s)\n%s", err, string(req), stack)
log.Debugf("server: %s (%s)\n%s", err, string(req), stack)
c.AbortWithStatus(http.StatusInternalServerError)
}
}()

View file

@ -5,8 +5,10 @@ import (
"path/filepath"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/api"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/pkg/clean"
)
func registerRoutes(router *gin.Engine, conf *config.Config) {
@ -192,6 +194,15 @@ func registerRoutes(router *gin.Engine, conf *config.Config) {
}
}
// Initialize package extensions.
for _, ext := range Extensions() {
if err := ext.init(router, conf); err != nil {
log.Warnf("server: failed to initialize extension %s (%s)", clean.Log(ext.name), err)
} else {
log.Debugf("server: extension %s initialized", clean.Log(ext.name))
}
}
// Default HTML page for client-side rendering and routing via VueJS.
router.NoRoute(func(c *gin.Context) {
signUp := gin.H{"message": config.MsgSponsor, "url": config.SignUpURL}

View file

@ -82,7 +82,7 @@ func (s *security) process(w http.ResponseWriter, r *http.Request) error {
if !isGoodHost {
s.opt.BadHostHandler.ServeHTTP(w, r)
return fmt.Errorf("http: bad host %s", clean.Log(r.Host))
return fmt.Errorf("server: bad host %s", clean.Log(r.Host))
}
}
@ -115,7 +115,7 @@ func (s *security) process(w http.ResponseWriter, r *http.Request) error {
}
http.Redirect(w, r, url.String(), status)
return fmt.Errorf("http: https redirect")
return fmt.Errorf("server: https redirect")
}
}

View file

@ -8,6 +8,7 @@ import (
"github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/event"
)
@ -55,7 +56,7 @@ func Start(ctx context.Context, conf *config.Config) {
// Enable HTTP compression?
switch conf.HttpCompression() {
case "gzip":
log.Infof("http: enabling gzip compression")
log.Infof("server: enabling gzip compression")
router.Use(gzip.Gzip(
gzip.DefaultCompression,
gzip.WithExcludedPaths([]string{
@ -68,8 +69,8 @@ func Start(ctx context.Context, conf *config.Config) {
})))
}
// Set template directory
router.LoadHTMLGlob(conf.TemplatesPath() + "/*")
// Find and load templates.
router.LoadHTMLFiles(conf.TemplateFiles()...)
// Register HTTP route handlers.
registerRoutes(router, conf)
@ -80,26 +81,26 @@ func Start(ctx context.Context, conf *config.Config) {
Handler: router,
}
log.Debugf("http: successfully initialized [%s]", time.Since(start))
log.Debugf("server: successfully initialized [%s]", time.Since(start))
// Start HTTP server.
go func() {
log.Infof("http: starting web server at %s", server.Addr)
log.Infof("server: listening at %s", server.Addr)
if err := server.ListenAndServe(); err != nil {
if err == http.ErrServerClosed {
log.Info("http: web server shutdown complete")
log.Info("server: shutdown complete")
} else {
log.Errorf("http: web server closed unexpect: %s", err)
log.Errorf("server: %s", err)
}
}
}()
// Graceful HTTP server shutdown.
<-ctx.Done()
log.Info("http: shutting down web server")
log.Info("server: shutting down")
err := server.Close()
if err != nil {
log.Errorf("http: web server shutdown failed: %v", err)
log.Errorf("server: shutdown failed (%s)", err)
}
}

View file

@ -0,0 +1,37 @@
package server
import (
"sync"
"sync/atomic"
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/config"
)
var (
extMutex sync.Mutex
extensions atomic.Value
)
// Extension represents a named package extension with callbacks.
type Extension struct {
name string
init func(router *gin.Engine, conf *config.Config) error
}
// Register registers a new package extension.
func Register(name string, init func(router *gin.Engine, conf *config.Config) error) {
extMutex.Lock()
h, _ := extensions.Load().([]Extension)
extensions.Store(append(h, Extension{name, init}))
extMutex.Unlock()
}
// Extensions returns all registered package extensions.
func Extensions() (ext []Extension) {
extMutex.Lock()
ext, _ = extensions.Load().([]Extension)
extMutex.Unlock()
return ext
}