- The roadmap shows what tasks are in progress, what needs testing, and which feature requests are going to be implemented next.
+
+ 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.
+
+
+
+
+ Your continued support helps us provide regular updates and remain independent, so we can fulfill our mission and protect your privacy.
+
+
+ Sponsors get access to additional features, receive direct technical support via email, and can join our private chat room on matrix.org.
+
+ 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.
+
+
+
-
-
+
User Guide
+
+ Visit docs.photoprism.app/user-guide to learn how to sync, organize, and share your pictures.
+ Our User Guide also covers many advanced topics, such as migrating from Google Photos and thumbnail quality settings.
+ Common issues can be quickly diagnosed and solved using the troubleshooting checklists we provide.
+ Read the docs ›
-
- Your continued support helps us provide regular updates and services like world maps.
- Sponsors get access to additional features, receive direct technical support via email, and can join our private chat room on matrix.org.
-
-
-
- Also, please leave a star on GitHub if you like this project. It provides additional motivation to keep going.
-
-
-
+ Knowledge Base
+ Browse the Knowledge Base for detailed information on specific product features, services, and related resources.
+ Learn more ›
+
+
Getting Support
-
+
- Before submitting a support request, please use our Troubleshooting Checklists to determine the cause of your problem.
- If this doesn't help, or you have other questions:
+ Before submitting a support request, please use our Troubleshooting Checklists to determine the cause of your problem.
+ If this doesn't help, or you have other questions:
-
+
In addition, sponsors receive direct technical support via email.
-
-
-
- We'll do our best to answer all your questions. In return, we ask you to back us on Patreon or GitHub Sponsors.
+
+ We'll do our best to answer all your questions. In return, we ask you to back us on Patreon or GitHub Sponsors.
+
diff --git a/frontend/src/pages/about/feedback.vue b/frontend/src/pages/about/feedback.vue
index 72114ae05..02881785e 100644
--- a/frontend/src/pages/about/feedback.vue
+++ b/frontend/src/pages/about/feedback.vue
@@ -1,10 +1,7 @@
-
- Your message has been sent
-
-
+
Contact Us
@@ -13,7 +10,16 @@
- We'll get back to you as soon as possible!
+
+ We appreciate your feedback!
+
+
+ Due to the high volume of emails we receive, our team may be unable to get back to you immediately.
+ We do our best to respond within five business days or less.
+
+
+
+
-
+
@@ -43,10 +43,10 @@
@keyup.enter.native="login"
>
-
+
-
+
Sign in
arrow_forward
@@ -58,15 +58,18 @@
-
- {{ config.siteCaption ? config.siteCaption : config.siteTitle }}
+
+ {{ $config.getEdition() }}
+
{{ config.imprint }}
{{ config.imprint }}
+
+ {{ config.siteCaption ? config.siteCaption : config.siteTitle }}
+
diff --git a/go.mod b/go.mod
index 6e9112f77..4ac0417f0 100644
--- a/go.mod
+++ b/go.mod
@@ -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
diff --git a/go.sum b/go.sum
index e9708d4ea..28263267d 100644
--- a/go.sum
+++ b/go.sum
@@ -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=
diff --git a/internal/commands/users.go b/internal/commands/users.go
index b0725223f..fa74b669b 100644
--- a/internal/commands/users.go
+++ b/internal/commands/users.go
@@ -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",
diff --git a/internal/config/cli_flag.go b/internal/config/cli_flag.go
index cb59442b0..a49cb67c7 100644
--- a/internal/config/cli_flag.go
+++ b/internal/config/cli_flag.go
@@ -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) {
diff --git a/internal/config/cli_flags.go b/internal/config/cli_flags.go
index f9dcfcb19..ea8bee3dd 100644
--- a/internal/config/cli_flags.go
+++ b/internal/config/cli_flags.go
@@ -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...)
+
+}
diff --git a/internal/config/cli_flags_report.go b/internal/config/cli_flags_report.go
index e7752ce09..4fa4ae204 100644
--- a/internal/config/cli_flags_report.go
+++ b/internal/config/cli_flags_report.go
@@ -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
diff --git a/internal/config/client_config.go b/internal/config/client_config.go
index 3f700df47..064eae96f 100644
--- a/internal/config/client_config.go
+++ b/internal/config/client_config.go
@@ -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().
diff --git a/internal/config/client_ext.go b/internal/config/client_ext.go
new file mode 100644
index 000000000..59971c88a
--- /dev/null
+++ b/internal/config/client_ext.go
@@ -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
+}
diff --git a/internal/config/config.go b/internal/config/config.go
index 7eabdf4a0..010011dd7 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -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 {
diff --git a/internal/config/config_customize.go b/internal/config/config_customize.go
index d1a3f921f..c96f40e5f 100644
--- a/internal/config/config_customize.go
+++ b/internal/config/config_customize.go
@@ -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 {
diff --git a/internal/config/config_customize_test.go b/internal/config/config_customize_test.go
index 3ea26173b..fbe79420d 100644
--- a/internal/config/config_customize_test.go
+++ b/internal/config/config_customize_test.go
@@ -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())
}
diff --git a/internal/config/config_ext.go b/internal/config/config_ext.go
new file mode 100644
index 000000000..63617f6d5
--- /dev/null
+++ b/internal/config/config_ext.go
@@ -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
+}
diff --git a/internal/config/config_faces.go b/internal/config/config_faces.go
index 398725d2c..b5934a7f6 100644
--- a/internal/config/config_faces.go
+++ b/internal/config/config_faces.go
@@ -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
}
diff --git a/internal/config/config_features.go b/internal/config/config_features.go
index 8d67c33b7..794c69b2e 100644
--- a/internal/config/config_features.go
+++ b/internal/config/config_features.go
@@ -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 {
diff --git a/internal/config/config_ffmpeg.go b/internal/config/config_ffmpeg.go
index 503c1a193..b80beb0aa 100644
--- a/internal/config/config_ffmpeg.go
+++ b/internal/config/config_ffmpeg.go
@@ -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)
}
diff --git a/internal/config/config_filepaths.go b/internal/config/config_filepaths.go
index 91945cf09..37bafee40 100644
--- a/internal/config/config_filepaths.go
+++ b/internal/config/config_filepaths.go
@@ -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")
diff --git a/internal/config/config_report.go b/internal/config/config_report.go
index d61208b11..52422a4aa 100644
--- a/internal/config/config_report.go
+++ b/internal/config/config_report.go
@@ -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
}
diff --git a/internal/config/config_server.go b/internal/config/config_server.go
index 5db3b5e36..013b6ab74 100644
--- a/internal/config/config_server.go
+++ b/internal/config/config_server.go
@@ -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).
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index 67db87141..d603336aa 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -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) {
diff --git a/internal/config/global_flags.go b/internal/config/global_flags.go
index 6a6698758..3f423cb63 100644
--- a/internal/config/global_flags.go
+++ b/internal/config/global_flags.go
@@ -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",
diff --git a/internal/config/global_options.go b/internal/config/global_options.go
index 35a42d5a6..10d622910 100644
--- a/internal/config/global_options.go
+++ b/internal/config/global_options.go
@@ -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"`
diff --git a/internal/config/test.go b/internal/config/test.go
index 17aaa95b4..be78063d2 100644
--- a/internal/config/test.go
+++ b/internal/config/test.go
@@ -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)))
diff --git a/internal/config/values.go b/internal/config/values.go
new file mode 100644
index 000000000..a9bc54231
--- /dev/null
+++ b/internal/config/values.go
@@ -0,0 +1,4 @@
+package config
+
+// Values is a shortcut for map[string]interface{}
+type Values map[string]interface{}
diff --git a/internal/server/logger.go b/internal/server/logger.go
index a8c227a69..fe1adbf25 100644
--- a/internal/server/logger.go
+++ b/internal/server/logger.go
@@ -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,
diff --git a/internal/server/recovery.go b/internal/server/recovery.go
index a4d459b4b..b736fc48f 100644
--- a/internal/server/recovery.go
+++ b/internal/server/recovery.go
@@ -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)
}
}()
diff --git a/internal/server/routes.go b/internal/server/routes.go
index a0ddfaa5e..c2d85a16b 100644
--- a/internal/server/routes.go
+++ b/internal/server/routes.go
@@ -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}
diff --git a/internal/server/security.go b/internal/server/security.go
index 547f12163..d76a835be 100644
--- a/internal/server/security.go
+++ b/internal/server/security.go
@@ -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")
}
}
diff --git a/internal/server/server.go b/internal/server/server.go
index 19069d133..71ae72e4d 100644
--- a/internal/server/server.go
+++ b/internal/server/server.go
@@ -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)
}
}
diff --git a/internal/server/server_ext.go b/internal/server/server_ext.go
new file mode 100644
index 000000000..a6f83ae99
--- /dev/null
+++ b/internal/server/server_ext.go
@@ -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
+}