Frontend: Refactor asset loading #1648

This commit is contained in:
Michael Mayer 2022-01-18 12:26:33 +01:00
parent 8998555da5
commit 03457bdb75
19 changed files with 465 additions and 616 deletions

View file

@ -26,8 +26,8 @@
{{template "favicons.tmpl" .}}
<link rel="stylesheet" href="{{ .config.StaticUri }}/build/app.css?{{ .config.CSSHash }}">
<link rel="manifest" href="{{ .config.BaseUri }}/manifest.json?{{ .config.ManifestHash }}" crossorigin="use-credentials">
<link rel="stylesheet" href="{{ .config.CssUri }}">
<link rel="manifest" href="{{ .config.ManifestUri }}" crossorigin="use-credentials">
<script>
window.__CONFIG__ = {{ .config }};
@ -37,6 +37,6 @@
{{template "app.tmpl" .}}
<script src="{{ .config.StaticUri }}/build/app.js?{{ .config.JSHash }}"></script>
<script src="{{ .config.JsUri }}"></script>
</body>
</html>

View file

@ -9,8 +9,8 @@
{{template "favicons.tmpl" .}}
<link rel="stylesheet" href="{{ .config.StaticUri }}/build/app.css?{{ .config.CSSHash }}">
<link rel="manifest" href="{{ .config.BaseUri }}/manifest.json?{{ .config.ManifestHash }}" crossorigin="use-credentials">
<link rel="stylesheet" href="{{ .config.CssUri }}">
<link rel="manifest" href="{{ .config.ManifestUri }}" crossorigin="use-credentials">
<script>
window.__CONFIG__ = {{ .config }};
@ -20,6 +20,6 @@
{{template "app.tmpl" .}}
<script src="{{ .config.StaticUri }}/build/app.js?{{ .config.JSHash }}"></script>
<script src="{{ .config.JsUri }}"></script>
</body>
</html>

View file

@ -12,7 +12,7 @@
{{template "favicons.tmpl" .}}
<link rel="stylesheet" href="{{ .config.StaticUri }}/build/app.css?{{ .config.CSSHash }}">
<link rel="stylesheet" href="{{ .config.CssUri }}">
</head>
<body>
<div id="photoprism" class="container">

View file

@ -23,8 +23,8 @@
{{template "favicons.tmpl" .}}
<link rel="stylesheet" href="{{ .config.StaticUri }}/build/share.css?{{ .config.CSSHash }}">
<link rel="manifest" href="{{ .config.BaseUri }}/manifest.json?{{ .config.ManifestHash }}" crossorigin="use-credentials">
<link rel="stylesheet" href="{{ .config.CssUri }}">
<link rel="manifest" href="{{ .config.ManifestUri }}" crossorigin="use-credentials">
<script>
window.__CONFIG__ = {{ .config }};
@ -34,6 +34,6 @@
{{template "app.tmpl" .}}
<script src="{{ .config.StaticUri }}/build/share.js?{{ .config.JSHash }}"></script>
<script src="{{ .config.JsUri }}"></script>
</body>
</html>

View file

@ -28,8 +28,8 @@
{{template "favicons.tmpl" .}}
<link rel="stylesheet" href="{{ .config.StaticUri }}/build/app.css?{{ .config.CSSHash }}">
<link rel="manifest" href="{{ .config.BaseUri }}/manifest.json?{{ .config.ManifestHash }}" crossorigin="use-credentials">
<link rel="stylesheet" href="{{ .config.CssUri }}">
<link rel="manifest" href="{{ .config.ManifestUri }}" crossorigin="use-credentials">
<script>
window.__CONFIG__ = {{ .config }};
@ -45,6 +45,6 @@
{{template "app.tmpl" .}}
<script src="{{ .config.StaticUri }}/build/app.js?{{ .config.JSHash }}"></script>
<script src="{{ .config.JsUri }}"></script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -79,7 +79,6 @@
"node-storage-shim": "^2.0.1",
"photoswipe": "^4.1.3",
"postcss": "^8.4.5",
"postcss-browser-reporter": "^0.6.0",
"postcss-import": "^14.0.2",
"postcss-loader": "^6.2.1",
"postcss-preset-env": "^7.2.3",
@ -87,8 +86,8 @@
"postcss-url": "^10.1.3",
"prettier": "^2.5.1",
"pubsub-js": "^1.9.4",
"regenerator-runtime": "^0.13.7",
"resolve-url-loader": "^3.1.4",
"regenerator-runtime": "^0.13.9",
"resolve-url-loader": "^5.0.0",
"sass": "^1.48.0",
"sass-loader": "^12.4.0",
"server": "^1.0.36",
@ -114,6 +113,7 @@
"webpack-bundle-analyzer": "^4.5.0",
"webpack-cli": "^4.9.1",
"webpack-hot-middleware": "^2.25.1",
"webpack-manifest-plugin": "^4.1.1",
"webpack-md5-hash": "^0.0.6",
"webpack-merge": "^5.8.0"
},

View file

@ -41,8 +41,9 @@ const testConfig = {
debug: false,
previewToken: "public",
downloadToken: "public",
jsHash: "00000000",
cssHash: "00000000",
cssUri: "/static/build/app.2259c0edcc020e7af593.css",
jsUri: "/static/build/app.9bd7132eaee8e4c7c7e3.js",
manifestUri: "/manifest.json?0e41a7e5",
};
const config = window.__CONFIG__ ? window.__CONFIG__ : testConfig;
@ -52,7 +53,7 @@ const Api = Axios.create({
headers: {
common: {
"X-Session-ID": window.localStorage.getItem("session_id"),
"X-Client-Hash": config.jsHash,
"X-Client-Uri": config.jsUri,
"X-Client-Version": config.version,
},
},

View file

@ -123,7 +123,7 @@ export default class Config {
console.log("config: new values", values);
}
if (values.jsHash && this.values.jsHash !== values.jsHash) {
if (values.jsUri && this.values.jsUri !== values.jsUri) {
Event.publish("dialog.reload", { values });
}

View file

@ -189,8 +189,8 @@ export default class Session {
const hasConfig = !!window.__CONFIG__;
const clientInfo = {
session: this.getId(),
js: hasConfig ? window.__CONFIG__.jsHash : "",
css: hasConfig ? window.__CONFIG__.cssHash : "",
cssUri: hasConfig ? window.__CONFIG__.cssUri : "",
jsUri: hasConfig ? window.__CONFIG__.jsUri : "",
version: hasConfig ? window.__CONFIG__.version : "",
};

View file

@ -283,9 +283,9 @@ const clientConfig = {
mapKey: "D9ve6edlcVR2mEsNvCXa",
downloadToken: "2lbh9x09",
previewToken: "public",
jsHash: "4b3198d7",
cssHash: "62f08685",
manifestHash: "3c637fa8",
cssUri: "/static/build/app.2259c0edcc020e7af593.css",
jsUri: "/static/build/app.9bd7132eaee8e4c7c7e3.js",
manifestUri: "/manifest.json?0e41a7e5",
settings: {
ui: {
scrollbar: true,

View file

@ -31,6 +31,7 @@ https://docs.photoprism.app/developer-guide/
const path = require("path");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const OfflinePlugin = require("@lcdp/offline-plugin");
const webpack = require("webpack");
const isDev = process.env.NODE_ENV !== "production";
@ -64,7 +65,8 @@ const config = {
output: {
path: PATHS.build,
publicPath: PATHS.public,
filename: "[name].js",
filename: "[name].[contenthash].js",
clean: true,
},
resolve: {
modules: [path.join(__dirname, "src"), path.join(__dirname, "node_modules")],
@ -74,9 +76,13 @@ const config = {
},
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
filename: "[name].[contenthash].css",
experimentalUseImportModule: false,
}),
new WebpackManifestPlugin({
fileName: "assets.json",
publicPath: "",
}),
new webpack.ProgressPlugin(),
new VueLoaderPlugin(),
new OfflinePlugin({

View file

@ -24,9 +24,8 @@ var wsTimeout = 90 * time.Second
type clientInfo struct {
SessionToken string `json:"session"`
JsHash string `json:"js"`
CssHash string `json:"css"`
ManifestHash string `json:"manifest"`
CssUri string `json:"css"`
JsUri string `json:"js"`
Version string `json:"version"`
}

View file

@ -4,11 +4,9 @@ import (
"strings"
"time"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/colors"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
@ -21,6 +19,9 @@ type ClientConfig struct {
Flags string `json:"flags"`
BaseUri string `json:"baseUri"`
StaticUri string `json:"staticUri"`
CssUri string `json:"cssUri"`
JsUri string `json:"jsUri"`
ManifestUri string `json:"manifestUri"`
ApiUri string `json:"apiUri"`
ContentUri string `json:"contentUri"`
SiteUrl string `json:"siteUrl"`
@ -52,9 +53,6 @@ type ClientConfig struct {
MapKey string `json:"mapKey"`
DownloadToken string `json:"downloadToken"`
PreviewToken string `json:"previewToken"`
JSHash string `json:"jsHash"`
CSSHash string `json:"cssHash"`
ManifestHash string `json:"manifestHash"`
Settings Settings `json:"settings"`
Disable ClientDisable `json:"disable"`
Count ClientCounts `json:"count"`
@ -167,6 +165,7 @@ func (c *Config) PublicConfig() ClientConfig {
return c.UserConfig()
}
assets := c.ClientAssets()
settings := c.Settings()
result := ClientConfig{
@ -196,6 +195,8 @@ func (c *Config) PublicConfig() ClientConfig {
Name: c.Name(),
BaseUri: c.BaseUri(""),
StaticUri: c.StaticUri(),
CssUri: assets.AppCssUri(),
JsUri: assets.AppJsUri(),
ApiUri: c.ApiUri(),
ContentUri: c.ContentUri(),
SiteUrl: c.SiteUrl(),
@ -221,9 +222,7 @@ func (c *Config) PublicConfig() ClientConfig {
MapKey: "",
Thumbs: Thumbs,
Colors: colors.All.List(),
JSHash: fs.Checksum(c.BuildPath() + "/app.js"),
CSSHash: fs.Checksum(c.BuildPath() + "/app.css"),
ManifestHash: fs.Checksum(c.TemplatesPath() + "/manifest.json"),
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
PreviewToken: "public",
DownloadToken: "public",
@ -234,6 +233,7 @@ func (c *Config) PublicConfig() ClientConfig {
// GuestConfig returns client config options for the sharing with guests.
func (c *Config) GuestConfig() ClientConfig {
assets := c.ClientAssets()
settings := c.Settings()
result := ClientConfig{
@ -263,6 +263,8 @@ func (c *Config) GuestConfig() ClientConfig {
Name: c.Name(),
BaseUri: c.BaseUri(""),
StaticUri: c.StaticUri(),
CssUri: assets.ShareCssUri(),
JsUri: assets.ShareJsUri(),
ApiUri: c.ApiUri(),
ContentUri: c.ContentUri(),
SiteUrl: c.SiteUrl(),
@ -291,9 +293,7 @@ func (c *Config) GuestConfig() ClientConfig {
MapKey: c.Hub().MapKey(),
DownloadToken: c.DownloadToken(),
PreviewToken: c.PreviewToken(),
JSHash: fs.Checksum(c.BuildPath() + "/share.js"),
CSSHash: fs.Checksum(c.BuildPath() + "/share.css"),
ManifestHash: fs.Checksum(c.TemplatesPath() + "/manifest.json"),
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
}
@ -302,6 +302,8 @@ func (c *Config) GuestConfig() ClientConfig {
// UserConfig returns client configuration options for registered users.
func (c *Config) UserConfig() ClientConfig {
assets := c.ClientAssets()
result := ClientConfig{
Settings: *c.Settings(),
Disable: ClientDisable{
@ -324,6 +326,8 @@ func (c *Config) UserConfig() ClientConfig {
Name: c.Name(),
BaseUri: c.BaseUri(""),
StaticUri: c.StaticUri(),
CssUri: assets.AppCssUri(),
JsUri: assets.AppJsUri(),
ApiUri: c.ApiUri(),
ContentUri: c.ContentUri(),
SiteUrl: c.SiteUrl(),
@ -352,9 +356,7 @@ func (c *Config) UserConfig() ClientConfig {
MapKey: c.Hub().MapKey(),
DownloadToken: c.DownloadToken(),
PreviewToken: c.PreviewToken(),
JSHash: fs.Checksum(c.BuildPath() + "/app.js"),
CSSHash: fs.Checksum(c.BuildPath() + "/app.css"),
ManifestHash: fs.Checksum(c.TemplatesPath() + "/manifest.json"),
ManifestUri: c.ClientManifestUri(),
Clip: txt.ClipDefault,
Server: NewRuntimeInfo(),
}

View file

@ -0,0 +1,91 @@
package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/photoprism/photoprism/pkg/fs"
)
type ClientAssets struct {
BaseUri string `json:"-"`
AppCss string `json:"app.css"`
AppJs string `json:"app.js"`
ShareCss string `json:"share.css"`
ShareJs string `json:"share.js"`
MaterialIconsRegularTtf string `json:"MaterialIcons-Regular.ttf"`
MaterialIconsRegularWoff string `json:"MaterialIcons-Regular.woff"`
MaterialIconsRegularEot string `json:"MaterialIcons-Regular.eot"`
MaterialIconsRegularWoff2 string `json:"MaterialIcons-Regular.woff2"`
OfflineServiceworker string `json:"__offline_serviceworker"`
DefaultSkinSvg string `json:"default-skin.svg"`
PreloaderGif string `json:"preloader.gif"`
DefaultSkinPng string `json:"default-skin.png"`
}
// NewClientAssets creates a new ClientAssets instance.
func NewClientAssets(baseUri string) ClientAssets {
return ClientAssets{BaseUri: baseUri}
}
// Load loads the frontend assets from a webpack manifest file.
func (a *ClientAssets) Load(fileName string) error {
jsonFile, err := os.ReadFile(fileName)
if err != nil {
return err
}
return json.Unmarshal(jsonFile, a)
}
// AppCssUri returns the web app stylesheet URI.
func (a *ClientAssets) AppCssUri() string {
if a.AppCss == "" {
return ""
}
return fmt.Sprintf("%s/build/%s", a.BaseUri, a.AppCss)
}
// AppJsUri returns the web app javascript URI.
func (a *ClientAssets) AppJsUri() string {
if a.AppJs == "" {
return ""
}
return fmt.Sprintf("%s/build/%s", a.BaseUri, a.AppJs)
}
// ShareCssUri returns the web sharing stylesheet URI.
func (a *ClientAssets) ShareCssUri() string {
if a.ShareCss == "" {
return ""
}
return fmt.Sprintf("%s/build/%s", a.BaseUri, a.ShareCss)
}
// ShareJsUri returns the web sharing javascript URI.
func (a *ClientAssets) ShareJsUri() string {
if a.ShareJs == "" {
return ""
}
return fmt.Sprintf("%s/build/%s", a.BaseUri, a.ShareJs)
}
// ClientAssets returns the frontend build assets.
func (c *Config) ClientAssets() ClientAssets {
result := NewClientAssets(c.StaticUri())
if err := result.Load(filepath.Join(c.BuildPath(), "assets.json")); err != nil {
log.Debugf("frontend: %s", err)
log.Errorf("frontend: cannot read assets.json")
}
return result
}
// ClientManifestUri returns the frontend manifest.json URI.
func (c *Config) ClientManifestUri() string {
return fmt.Sprintf("%s?%d", c.BaseUri("/manifest.json"), fs.BirthTime(c.TemplatesPath()+"/manifest.json").Unix())
}

View file

@ -0,0 +1,96 @@
package config
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestClientAssets_Load(t *testing.T) {
c := NewConfig(CliTestContext())
t.Run("Success", func(t *testing.T) {
a := NewClientAssets(c.StaticUri())
err := a.Load("testdata/static/build/assets.json")
assert.NoError(t, err)
assert.Equal(t, "/static", a.BaseUri)
assert.Equal(t, "app.2259c0edcc020e7af593.css", a.AppCss)
assert.Equal(t, "/static/build/app.2259c0edcc020e7af593.css", a.AppCssUri())
assert.Equal(t, "app.9bd7132eaee8e4c7c7e3.js", a.AppJs)
assert.Equal(t, "/static/build/app.9bd7132eaee8e4c7c7e3.js", a.AppJsUri())
assert.Equal(t, "share.2259c0edcc020e7af593.css", a.ShareCss)
assert.Equal(t, "/static/build/share.2259c0edcc020e7af593.css", a.ShareCssUri())
assert.Equal(t, "share.7aaf321a984ae545e4e5.js", a.ShareJs)
assert.Equal(t, "/static/build/share.7aaf321a984ae545e4e5.js", a.ShareJsUri())
})
t.Run("Error", func(t *testing.T) {
a := NewClientAssets(c.StaticUri())
err := a.Load("testdata/foo/assets.json")
assert.Error(t, err)
assert.Equal(t, "/static", a.BaseUri)
assert.Equal(t, "", a.AppCss)
assert.Equal(t, "", a.AppCssUri())
assert.Equal(t, "", a.AppJs)
assert.Equal(t, "", a.AppJsUri())
assert.Equal(t, "", a.ShareCss)
assert.Equal(t, "", a.ShareCssUri())
assert.Equal(t, "", a.ShareJs)
assert.Equal(t, "", a.ShareJsUri())
})
}
func TestConfig_ClientAssets(t *testing.T) {
c := NewConfig(CliTestContext())
c.options.AssetsPath = "testdata"
c.options.CdnUrl = "https://mycdn.com/foo/"
a := c.ClientAssets()
assert.Equal(t, "https://mycdn.com/foo/static", a.BaseUri)
assert.Equal(t, "app.2259c0edcc020e7af593.css", a.AppCss)
assert.Equal(t, "https://mycdn.com/foo/static/build/app.2259c0edcc020e7af593.css", a.AppCssUri())
assert.Equal(t, "app.9bd7132eaee8e4c7c7e3.js", a.AppJs)
assert.Equal(t, "https://mycdn.com/foo/static/build/app.9bd7132eaee8e4c7c7e3.js", a.AppJsUri())
assert.Equal(t, "share.2259c0edcc020e7af593.css", a.ShareCss)
assert.Equal(t, "https://mycdn.com/foo/static/build/share.2259c0edcc020e7af593.css", a.ShareCssUri())
assert.Equal(t, "share.7aaf321a984ae545e4e5.js", a.ShareJs)
assert.Equal(t, "https://mycdn.com/foo/static/build/share.7aaf321a984ae545e4e5.js", a.ShareJsUri())
c.options.AssetsPath = "testdata/invalid"
c.options.CdnUrl = ""
c.options.SiteUrl = "http://myhost/foo"
a = c.ClientAssets()
assert.Equal(t, "/foo/static", a.BaseUri)
assert.Equal(t, "", a.AppCss)
assert.Equal(t, "", a.AppCssUri())
assert.Equal(t, "", a.AppJs)
assert.Equal(t, "", a.AppJsUri())
assert.Equal(t, "", a.ShareCss)
assert.Equal(t, "", a.ShareCssUri())
assert.Equal(t, "", a.ShareJs)
assert.Equal(t, "", a.ShareJsUri())
}
func TestClientManifestUri(t *testing.T) {
c := NewConfig(CliTestContext())
assert.Equal(t, "/manifest.json?1638035864", c.ClientManifestUri())
c.options.SiteUrl = ""
assert.Equal(t, "/manifest.json?1638035864", c.ClientManifestUri())
c.options.SiteUrl = "http://myhost/foo"
assert.Equal(t, "/foo/manifest.json?1638035864", c.ClientManifestUri())
}

View file

@ -252,9 +252,9 @@ func TestConfig_ClientConfig(t *testing.T) {
assert.NotEmpty(t, cc.Version)
assert.NotEmpty(t, cc.Copyright)
assert.NotEmpty(t, cc.Thumbs)
assert.NotEmpty(t, cc.JSHash)
assert.NotEmpty(t, cc.CSSHash)
assert.NotEmpty(t, cc.ManifestHash)
assert.NotEmpty(t, cc.JsUri)
assert.NotEmpty(t, cc.CssUri)
assert.NotEmpty(t, cc.ManifestUri)
assert.Equal(t, true, cc.Debug)
assert.Equal(t, false, cc.Demo)
assert.Equal(t, true, cc.Sponsor)

View file

@ -0,0 +1,14 @@
{
"app.css": "app.2259c0edcc020e7af593.css",
"app.js": "app.9bd7132eaee8e4c7c7e3.js",
"share.css": "share.2259c0edcc020e7af593.css",
"share.js": "share.7aaf321a984ae545e4e5.js",
"MaterialIcons-Regular.ttf": "194d1b9ef54d0014b3c4.ttf",
"MaterialIcons-Regular.woff": "aef30bd507a35c7688c7.woff",
"MaterialIcons-Regular.eot": "c4dac66977e8a757a3ca.eot",
"MaterialIcons-Regular.woff2": "1f0f6cdb23348c639cc5.woff2",
"__offline_serviceworker": "__offline_serviceworker",
"default-skin.svg": "2f15497756e1a99037f3.svg",
"preloader.gif": "14d740b6ee64510747b0.gif",
"default-skin.png": "0ff4d62150facee6b7de.png"
}

View file

@ -6,7 +6,7 @@ import (
"github.com/djherbis/times"
)
// BirthTime returns the create time of a file or folder.
// BirthTime returns the creation time of a file or folder.
func BirthTime(fileName string) time.Time {
s, err := times.Stat(fileName)