mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 08:44:04 +01:00
Config: Add StaticBuildFile() and StaticImgFile() functions #5274
Signed-off-by: Michael Mayer <michael@photoprism.app>
This commit is contained in:
@@ -135,7 +135,7 @@ func (a *ClientAssets) readFile(fileName string) string {
|
|||||||
|
|
||||||
// ClientAssets returns the frontend build assets.
|
// ClientAssets returns the frontend build assets.
|
||||||
func (c *Config) ClientAssets() *ClientAssets {
|
func (c *Config) ClientAssets() *ClientAssets {
|
||||||
result := NewClientAssets(c.BuildPath(), c.StaticUri())
|
result := NewClientAssets(c.StaticBuildPath(), c.StaticUri())
|
||||||
|
|
||||||
if err := result.Load("assets.json"); err != nil {
|
if err := result.Load("assets.json"); err != nil {
|
||||||
log.Debugf("frontend: %s", err)
|
log.Debugf("frontend: %s", err)
|
||||||
|
|||||||
@@ -277,16 +277,28 @@ func (c *Config) StaticFile(fileName string) string {
|
|||||||
return filepath.Join(c.AssetsPath(), fs.StaticDir, fileName)
|
return filepath.Join(c.AssetsPath(), fs.StaticDir, fileName)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildPath returns the static build path.
|
// StaticBuildPath returns the static build path.
|
||||||
func (c *Config) BuildPath() string {
|
func (c *Config) StaticBuildPath() string {
|
||||||
return filepath.Join(c.StaticPath(), fs.BuildDir)
|
return filepath.Join(c.StaticPath(), fs.BuildDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ImgPath returns the path to static image files.
|
// StaticBuildFile joins the static build directory with the given relative path and
|
||||||
func (c *Config) ImgPath() string {
|
// returns an absolute file system location (e.g. hashed bundles or sw.js).
|
||||||
|
func (c *Config) StaticBuildFile(fileName string) string {
|
||||||
|
return filepath.Join(c.StaticBuildPath(), fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// StaticImgPath returns the path to static image files.
|
||||||
|
func (c *Config) StaticImgPath() string {
|
||||||
return filepath.Join(c.StaticPath(), fs.ImgDir)
|
return filepath.Join(c.StaticPath(), fs.ImgDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StaticImgFile joins the static image directory with the given relative path and
|
||||||
|
// returns an absolute file system location (e.g. icons or wallpapers).
|
||||||
|
func (c *Config) StaticImgFile(fileName string) string {
|
||||||
|
return filepath.Join(c.StaticImgPath(), fileName)
|
||||||
|
}
|
||||||
|
|
||||||
// ThemePath returns the path to static theme files.
|
// ThemePath returns the path to static theme files.
|
||||||
func (c *Config) ThemePath() string {
|
func (c *Config) ThemePath() string {
|
||||||
if c.options.CustomThemePath != "" {
|
if c.options.CustomThemePath != "" {
|
||||||
|
|||||||
@@ -143,12 +143,12 @@ func (c *Config) SiteFavicon() string {
|
|||||||
return c.options.SiteFavicon
|
return c.options.SiteFavicon
|
||||||
} else if fileName := filepath.Join(c.ThemePath(), strings.TrimPrefix(c.options.SiteFavicon, ThemeUri)); fs.FileExistsNotEmpty(fileName) {
|
} else if fileName := filepath.Join(c.ThemePath(), strings.TrimPrefix(c.options.SiteFavicon, ThemeUri)); fs.FileExistsNotEmpty(fileName) {
|
||||||
return fileName
|
return fileName
|
||||||
} else if fileName = filepath.Join(c.ImgPath(), c.options.SiteFavicon); fs.FileExistsNotEmpty(fileName) {
|
} else if fileName = c.StaticImgFile(c.options.SiteFavicon); fs.FileExistsNotEmpty(fileName) {
|
||||||
return fileName
|
return fileName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(c.ImgPath(), "favicon.ico")
|
return c.StaticImgFile("favicon.ico")
|
||||||
}
|
}
|
||||||
|
|
||||||
// SitePreview returns the site preview image URL for sharing.
|
// SitePreview returns the site preview image URL for sharing.
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ func (c *Config) CreateDirectories() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create frontend build path if it doesn't exist yet.
|
// Create frontend build path if it doesn't exist yet.
|
||||||
if dir := c.BuildPath(); dir == "" {
|
if dir := c.StaticBuildPath(); dir == "" {
|
||||||
return notFoundError("build")
|
return notFoundError("build")
|
||||||
} else if err := fs.MkdirAll(dir); err != nil {
|
} else if err := fs.MkdirAll(dir); err != nil {
|
||||||
return createError(dir, err)
|
return createError(dir, err)
|
||||||
|
|||||||
@@ -256,20 +256,34 @@ func TestConfig_StaticFile(t *testing.T) {
|
|||||||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/video/404.mp4", path)
|
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/video/404.mp4", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_BuildPath(t *testing.T) {
|
func TestConfig_StaticBuildPath(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
path := c.BuildPath()
|
path := c.StaticBuildPath()
|
||||||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/build", path)
|
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/build", path)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConfig_ImgPath(t *testing.T) {
|
func TestConfig_StaticBuildFile(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
result := c.ImgPath()
|
assert.Equal(t, filepath.Join(c.StaticBuildPath(), fs.SwJsFile), c.StaticBuildFile(fs.SwJsFile))
|
||||||
|
assert.Equal(t, filepath.Join(c.StaticBuildPath(), "chunk/app.js"), c.StaticBuildFile("chunk/app.js"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig_StaticImgPath(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
result := c.StaticImgPath()
|
||||||
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/img", result)
|
assert.Equal(t, "/go/src/github.com/photoprism/photoprism/assets/static/img", result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestConfig_StaticImgFile(t *testing.T) {
|
||||||
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
assert.Equal(t, filepath.Join(c.StaticImgPath(), "favicon.ico"), c.StaticImgFile("favicon.ico"))
|
||||||
|
assert.Equal(t, filepath.Join(c.StaticImgPath(), "wallpapers/default.jpg"), c.StaticImgFile("/wallpapers/default.jpg"))
|
||||||
|
}
|
||||||
|
|
||||||
func TestConfig_ThemePath(t *testing.T) {
|
func TestConfig_ThemePath(t *testing.T) {
|
||||||
c := NewConfig(CliTestContext())
|
c := NewConfig(CliTestContext())
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package pwa
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/photoprism/photoprism/pkg/clean"
|
"github.com/photoprism/photoprism/pkg/clean"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/list"
|
"github.com/photoprism/photoprism/pkg/list"
|
||||||
"github.com/photoprism/photoprism/pkg/txt"
|
"github.com/photoprism/photoprism/pkg/txt"
|
||||||
)
|
)
|
||||||
@@ -51,7 +52,7 @@ func NewManifest(c Config) (m *Manifest) {
|
|||||||
StartUrl: c.BaseUri + "library/",
|
StartUrl: c.BaseUri + "library/",
|
||||||
Shortcuts: Shortcuts(c.BaseUri),
|
Shortcuts: Shortcuts(c.BaseUri),
|
||||||
Serviceworker: Serviceworker{
|
Serviceworker: Serviceworker{
|
||||||
Src: "sw.js",
|
Src: fs.SwJsFile,
|
||||||
Scope: c.BaseUri,
|
Scope: c.BaseUri,
|
||||||
UseCache: true,
|
UseCache: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
|||||||
{"assets-path", c.AssetsPath()},
|
{"assets-path", c.AssetsPath()},
|
||||||
{"models-path", c.ModelsPath()},
|
{"models-path", c.ModelsPath()},
|
||||||
{"static-path", c.StaticPath()},
|
{"static-path", c.StaticPath()},
|
||||||
{"build-path", c.BuildPath()},
|
{"static-build-path", c.StaticBuildPath()},
|
||||||
{"img-path", c.ImgPath()},
|
{"static-img-path", c.StaticImgPath()},
|
||||||
{"templates-path", c.TemplatesPath()},
|
{"templates-path", c.TemplatesPath()},
|
||||||
|
|
||||||
// Sidecar Files.
|
// Sidecar Files.
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStaticRoutes(t *testing.T) {
|
func TestStaticRoutes(t *testing.T) {
|
||||||
@@ -95,14 +96,14 @@ func TestWebAppRoutes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
t.Run("GetServiceWorker", func(t *testing.T) {
|
t.Run("GetServiceWorker", func(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
req, _ := http.NewRequest("GET", "/sw.js", nil)
|
req, _ := http.NewRequest("GET", "/"+fs.SwJsFile, nil)
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
assert.Equal(t, 200, w.Code)
|
assert.Equal(t, 200, w.Code)
|
||||||
assert.NotEmpty(t, w.Body)
|
assert.NotEmpty(t, w.Body)
|
||||||
})
|
})
|
||||||
t.Run("HeadServiceWorker", func(t *testing.T) {
|
t.Run("HeadServiceWorker", func(t *testing.T) {
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
req, _ := http.NewRequest("HEAD", "/sw.js", nil)
|
req, _ := http.NewRequest("HEAD", "/"+fs.SwJsFile, nil)
|
||||||
r.ServeHTTP(w, req)
|
r.ServeHTTP(w, req)
|
||||||
assert.Equal(t, 200, w.Code)
|
assert.Equal(t, 200, w.Code)
|
||||||
assert.Empty(t, w.Body)
|
assert.Empty(t, w.Body)
|
||||||
|
|||||||
@@ -2,13 +2,12 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"github.com/photoprism/photoprism/internal/api"
|
"github.com/photoprism/photoprism/internal/api"
|
||||||
"github.com/photoprism/photoprism/internal/config"
|
"github.com/photoprism/photoprism/internal/config"
|
||||||
|
"github.com/photoprism/photoprism/pkg/fs"
|
||||||
"github.com/photoprism/photoprism/pkg/service/http/header"
|
"github.com/photoprism/photoprism/pkg/service/http/header"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -54,23 +53,25 @@ func registerWebAppRoutes(router *gin.Engine, conf *config.Config) {
|
|||||||
swWorker := func(c *gin.Context) {
|
swWorker := func(c *gin.Context) {
|
||||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||||
|
|
||||||
if swBuildPath := filepath.Join(conf.BuildPath(), "sw.js"); swBuildPath != "" {
|
// Serve the Workbox-generated service worker when the frontend build has
|
||||||
if _, err := os.Stat(swBuildPath); err == nil {
|
// produced one (default for production builds).
|
||||||
c.File(swBuildPath)
|
if swFile := conf.StaticBuildFile(fs.SwJsFile); fs.FileExistsNotEmpty(swFile) {
|
||||||
return
|
c.File(swFile)
|
||||||
}
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fall back to the embedded no-op service worker so tests and dev builds
|
||||||
|
// still receive a valid response.
|
||||||
if len(fallbackServiceWorker) > 0 {
|
if len(fallbackServiceWorker) > 0 {
|
||||||
c.Data(http.StatusOK, "application/javascript", fallbackServiceWorker)
|
c.Data(http.StatusOK, header.ContentTypeJavaScript, fallbackServiceWorker)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Status(http.StatusNotFound)
|
c.Status(http.StatusNotFound)
|
||||||
}
|
}
|
||||||
router.Any("/sw.js", swWorker)
|
router.Any("/"+fs.SwJsFile, swWorker)
|
||||||
|
|
||||||
if swUri := conf.BaseUri("/sw.js"); swUri != "/sw.js" {
|
if swUri := conf.BaseUri("/" + fs.SwJsFile); swUri != "/"+fs.SwJsFile {
|
||||||
router.Any(swUri, swWorker)
|
router.Any(swUri, swWorker)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,10 @@ package server
|
|||||||
|
|
||||||
import _ "embed"
|
import _ "embed"
|
||||||
|
|
||||||
// fallbackServiceWorker contains a minimal no-op service worker used when a
|
// fallbackServiceWorker is a tiny service worker embedded in the binary so the
|
||||||
// Workbox-generated sw.js is not available (for example during tests or when
|
// server can respond to /sw.js even when the frontend assets (and thus the
|
||||||
// frontend assets have not been built yet).
|
// Workbox-generated service worker) have not been built yet. It keeps tests and
|
||||||
|
// development environments functional without affecting production builds.
|
||||||
//
|
//
|
||||||
//go:embed sw_fallback.js
|
//go:embed sw_fallback.js
|
||||||
var fallbackServiceWorker []byte
|
var fallbackServiceWorker []byte
|
||||||
|
|||||||
@@ -348,7 +348,10 @@ func mergeOptionsYaml(c *config.Config, updates Values) error {
|
|||||||
// local theme directory is missing or lacks an app.js file.
|
// local theme directory is missing or lacks an app.js file.
|
||||||
func installThemeIfMissing(c *config.Config, portal *url.URL, token string) error {
|
func installThemeIfMissing(c *config.Config, portal *url.URL, token string) error {
|
||||||
themeDir := c.ThemePath()
|
themeDir := c.ThemePath()
|
||||||
need := !fs.PathExists(themeDir) || (cluster.BootstrapThemeInstallOnlyIfMissingJS && !fs.FileExists(filepath.Join(themeDir, "app.js")))
|
|
||||||
|
need := !fs.PathExists(themeDir) ||
|
||||||
|
(cluster.BootstrapThemeInstallOnlyIfMissingJS && !fs.FileExists(filepath.Join(themeDir, fs.AppJsFile)))
|
||||||
|
|
||||||
if !need && !cluster.BootstrapAllowThemeOverwrite {
|
if !need && !cluster.BootstrapAllowThemeOverwrite {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ func TestThemeInstall_SkipWhenAppJsExists(t *testing.T) {
|
|||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
defer func() { _ = os.RemoveAll(tempTheme) }()
|
defer func() { _ = os.RemoveAll(tempTheme) }()
|
||||||
c.SetThemePath(tempTheme)
|
c.SetThemePath(tempTheme)
|
||||||
assert.NoError(t, os.WriteFile(filepath.Join(tempTheme, "app.js"), []byte("// app\n"), fs.ModeFile))
|
assert.NoError(t, os.WriteFile(filepath.Join(tempTheme, fs.AppJsFile), []byte("// app\n"), fs.ModeFile))
|
||||||
|
|
||||||
assert.NoError(t, InitConfig(c))
|
assert.NoError(t, InitConfig(c))
|
||||||
// Should have skipped request because app.js already exists.
|
// Should have skipped request because app.js already exists.
|
||||||
|
|||||||
@@ -38,3 +38,9 @@ const (
|
|||||||
UsersDir = "users"
|
UsersDir = "users"
|
||||||
ZipDir = "zip"
|
ZipDir = "zip"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Common file names used across packages (sorted by name).
|
||||||
|
const (
|
||||||
|
AppJsFile = "app.js"
|
||||||
|
SwJsFile = "sw.js"
|
||||||
|
)
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ func TestContent(t *testing.T) {
|
|||||||
assert.Equal(t, "multipart/form-data", ContentTypeMultipart)
|
assert.Equal(t, "multipart/form-data", ContentTypeMultipart)
|
||||||
assert.Equal(t, "application/json", ContentTypeJson)
|
assert.Equal(t, "application/json", ContentTypeJson)
|
||||||
assert.Equal(t, "application/json; charset=utf-8", ContentTypeJsonUtf8)
|
assert.Equal(t, "application/json; charset=utf-8", ContentTypeJsonUtf8)
|
||||||
|
assert.Equal(t, "application/javascript", ContentTypeJavaScript)
|
||||||
|
assert.Equal(t, "text/css", ContentTypeCSS)
|
||||||
assert.Equal(t, "text/html; charset=utf-8", ContentTypeHtml)
|
assert.Equal(t, "text/html; charset=utf-8", ContentTypeHtml)
|
||||||
assert.Equal(t, "text/plain; charset=utf-8", ContentTypeText)
|
assert.Equal(t, "text/plain; charset=utf-8", ContentTypeText)
|
||||||
assert.Equal(t, "image/png", ContentTypePng)
|
assert.Equal(t, "image/png", ContentTypePng)
|
||||||
|
|||||||
@@ -101,6 +101,8 @@ const (
|
|||||||
ContentTypeMultipart = "multipart/form-data"
|
ContentTypeMultipart = "multipart/form-data"
|
||||||
ContentTypeJson = "application/json"
|
ContentTypeJson = "application/json"
|
||||||
ContentTypeJsonUtf8 = "application/json; charset=utf-8"
|
ContentTypeJsonUtf8 = "application/json; charset=utf-8"
|
||||||
|
ContentTypeJavaScript = "application/javascript"
|
||||||
|
ContentTypeCSS = "text/css"
|
||||||
ContentTypeXml = "text/xml"
|
ContentTypeXml = "text/xml"
|
||||||
ContentTypeHtml = "text/html; charset=utf-8"
|
ContentTypeHtml = "text/html; charset=utf-8"
|
||||||
ContentTypeText = "text/plain; charset=utf-8"
|
ContentTypeText = "text/plain; charset=utf-8"
|
||||||
|
|||||||
Reference in New Issue
Block a user