mirror of
https://github.com/photoprism/photoprism.git
synced 2025-12-12 00:34:13 +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.
|
||||
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 {
|
||||
log.Debugf("frontend: %s", err)
|
||||
|
||||
@@ -277,16 +277,28 @@ func (c *Config) StaticFile(fileName string) string {
|
||||
return filepath.Join(c.AssetsPath(), fs.StaticDir, fileName)
|
||||
}
|
||||
|
||||
// BuildPath returns the static build path.
|
||||
func (c *Config) BuildPath() string {
|
||||
// StaticBuildPath returns the static build path.
|
||||
func (c *Config) StaticBuildPath() string {
|
||||
return filepath.Join(c.StaticPath(), fs.BuildDir)
|
||||
}
|
||||
|
||||
// ImgPath returns the path to static image files.
|
||||
func (c *Config) ImgPath() string {
|
||||
// StaticBuildFile joins the static build directory with the given relative path and
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (c *Config) ThemePath() string {
|
||||
if c.options.CustomThemePath != "" {
|
||||
|
||||
@@ -143,12 +143,12 @@ func (c *Config) SiteFavicon() string {
|
||||
return c.options.SiteFavicon
|
||||
} else if fileName := filepath.Join(c.ThemePath(), strings.TrimPrefix(c.options.SiteFavicon, ThemeUri)); fs.FileExistsNotEmpty(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 filepath.Join(c.ImgPath(), "favicon.ico")
|
||||
return c.StaticImgFile("favicon.ico")
|
||||
}
|
||||
|
||||
// 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.
|
||||
if dir := c.BuildPath(); dir == "" {
|
||||
if dir := c.StaticBuildPath(); dir == "" {
|
||||
return notFoundError("build")
|
||||
} else if err := fs.MkdirAll(dir); err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
func TestConfig_BuildPath(t *testing.T) {
|
||||
func TestConfig_StaticBuildPath(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
path := c.BuildPath()
|
||||
path := c.StaticBuildPath()
|
||||
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())
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
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) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@ package pwa
|
||||
|
||||
import (
|
||||
"github.com/photoprism/photoprism/pkg/clean"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/list"
|
||||
"github.com/photoprism/photoprism/pkg/txt"
|
||||
)
|
||||
@@ -51,7 +52,7 @@ func NewManifest(c Config) (m *Manifest) {
|
||||
StartUrl: c.BaseUri + "library/",
|
||||
Shortcuts: Shortcuts(c.BaseUri),
|
||||
Serviceworker: Serviceworker{
|
||||
Src: "sw.js",
|
||||
Src: fs.SwJsFile,
|
||||
Scope: c.BaseUri,
|
||||
UseCache: true,
|
||||
},
|
||||
|
||||
@@ -80,8 +80,8 @@ func (c *Config) Report() (rows [][]string, cols []string) {
|
||||
{"assets-path", c.AssetsPath()},
|
||||
{"models-path", c.ModelsPath()},
|
||||
{"static-path", c.StaticPath()},
|
||||
{"build-path", c.BuildPath()},
|
||||
{"img-path", c.ImgPath()},
|
||||
{"static-build-path", c.StaticBuildPath()},
|
||||
{"static-img-path", c.StaticImgPath()},
|
||||
{"templates-path", c.TemplatesPath()},
|
||||
|
||||
// Sidecar Files.
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
)
|
||||
|
||||
func TestStaticRoutes(t *testing.T) {
|
||||
@@ -95,14 +96,14 @@ func TestWebAppRoutes(t *testing.T) {
|
||||
})
|
||||
t.Run("GetServiceWorker", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("GET", "/sw.js", nil)
|
||||
req, _ := http.NewRequest("GET", "/"+fs.SwJsFile, nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.NotEmpty(t, w.Body)
|
||||
})
|
||||
t.Run("HeadServiceWorker", func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("HEAD", "/sw.js", nil)
|
||||
req, _ := http.NewRequest("HEAD", "/"+fs.SwJsFile, nil)
|
||||
r.ServeHTTP(w, req)
|
||||
assert.Equal(t, 200, w.Code)
|
||||
assert.Empty(t, w.Body)
|
||||
|
||||
@@ -2,13 +2,12 @@ package server
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/api"
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"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) {
|
||||
c.Header(header.CacheControl, header.CacheControlNoStore)
|
||||
|
||||
if swBuildPath := filepath.Join(conf.BuildPath(), "sw.js"); swBuildPath != "" {
|
||||
if _, err := os.Stat(swBuildPath); err == nil {
|
||||
c.File(swBuildPath)
|
||||
// Serve the Workbox-generated service worker when the frontend build has
|
||||
// produced one (default for production builds).
|
||||
if swFile := conf.StaticBuildFile(fs.SwJsFile); fs.FileExistsNotEmpty(swFile) {
|
||||
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 {
|
||||
c.Data(http.StatusOK, "application/javascript", fallbackServiceWorker)
|
||||
c.Data(http.StatusOK, header.ContentTypeJavaScript, fallbackServiceWorker)
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,10 @@ package server
|
||||
|
||||
import _ "embed"
|
||||
|
||||
// fallbackServiceWorker contains a minimal no-op service worker used when a
|
||||
// Workbox-generated sw.js is not available (for example during tests or when
|
||||
// frontend assets have not been built yet).
|
||||
// fallbackServiceWorker is a tiny service worker embedded in the binary so the
|
||||
// server can respond to /sw.js even when the frontend assets (and thus the
|
||||
// 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
|
||||
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.
|
||||
func installThemeIfMissing(c *config.Config, portal *url.URL, token string) error {
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ func TestThemeInstall_SkipWhenAppJsExists(t *testing.T) {
|
||||
assert.NoError(t, err)
|
||||
defer func() { _ = os.RemoveAll(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))
|
||||
// Should have skipped request because app.js already exists.
|
||||
|
||||
@@ -38,3 +38,9 @@ const (
|
||||
UsersDir = "users"
|
||||
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, "application/json", ContentTypeJson)
|
||||
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/plain; charset=utf-8", ContentTypeText)
|
||||
assert.Equal(t, "image/png", ContentTypePng)
|
||||
|
||||
@@ -101,6 +101,8 @@ const (
|
||||
ContentTypeMultipart = "multipart/form-data"
|
||||
ContentTypeJson = "application/json"
|
||||
ContentTypeJsonUtf8 = "application/json; charset=utf-8"
|
||||
ContentTypeJavaScript = "application/javascript"
|
||||
ContentTypeCSS = "text/css"
|
||||
ContentTypeXml = "text/xml"
|
||||
ContentTypeHtml = "text/html; charset=utf-8"
|
||||
ContentTypeText = "text/plain; charset=utf-8"
|
||||
|
||||
Reference in New Issue
Block a user