From caf2aaf4f9d38ad441a5562c3b8720f8779d6f78 Mon Sep 17 00:00:00 2001 From: Kim <1877318+kimsible@users.noreply.github.com> Date: Fri, 10 Jul 2020 10:20:11 +0200 Subject: [PATCH] Add ability to override client assets : logo - favicon - PWA icons - PWA manifest name and description (#2897) * Add client-overrides storage to config * Add static-serve for client overrides * Move backgroun-image logo from bundle to css tag for runtime content hash * Add dynamic JSON manifest * Add content hash for manifest, favicon and logo Co-authored-by: kimsible --- client/src/app/app.component.scss | 2 +- client/src/index.html | 11 +++- config/default.yaml | 5 ++ config/production.yaml.example | 5 ++ config/test-1.yaml | 1 + config/test-2.yaml | 1 + config/test-3.yaml | 1 + config/test-4.yaml | 1 + config/test-5.yaml | 1 + config/test-6.yaml | 1 + server/controllers/client.ts | 52 +++++++++++++++++-- server/initializers/config.ts | 3 +- server/initializers/constants.ts | 17 +++++- server/lib/client-html.ts | 17 +++++- .../docker/production/config/production.yaml | 5 ++ 15 files changed, 114 insertions(+), 9 deletions(-) diff --git a/client/src/app/app.component.scss b/client/src/app/app.component.scss index d121ebad2..38ec11b5b 100644 --- a/client/src/app/app.component.scss +++ b/client/src/app/app.component.scss @@ -62,7 +62,7 @@ .icon.icon-logo { display: inline-block; - background: url('../assets/images/logo.svg') no-repeat; + background-repeat: no-repeat; width: 23px; height: 24px; margin-right: .5rem; diff --git a/client/src/index.html b/client/src/index.html index 52ae000bb..e5d1569aa 100644 --- a/client/src/index.html +++ b/client/src/index.html @@ -7,9 +7,16 @@ - + - + + + + diff --git a/config/default.yaml b/config/default.yaml index a3df1bd45..d6f7f7afe 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -85,6 +85,11 @@ storage: captions: 'storage/captions/' cache: 'storage/cache/' plugins: 'storage/plugins/' + # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images + # Could contain for example assets/images/favicon.png + # If the file exists, peertube will serve it + # If not, peertube will fallback to the default fil + client_overrides: 'storage/client-overrides/' log: level: 'info' # debug/info/warning/error diff --git a/config/production.yaml.example b/config/production.yaml.example index a494bdb03..f57861eca 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example @@ -86,6 +86,11 @@ storage: captions: '/var/www/peertube/storage/captions/' cache: '/var/www/peertube/storage/cache/' plugins: '/var/www/peertube/storage/plugins/' + # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images + # Could contain for example assets/images/favicon.png + # If the file exists, peertube will serve it + # If not, peertube will fallback to the default fil + client_overrides: '/var/www/peertube/storage/client-overrides/' log: level: 'info' # debug/info/warning/error diff --git a/config/test-1.yaml b/config/test-1.yaml index 7b25f5cf3..2ef9e6c7c 100644 --- a/config/test-1.yaml +++ b/config/test-1.yaml @@ -22,6 +22,7 @@ storage: captions: 'test1/captions/' cache: 'test1/cache/' plugins: 'test1/plugins/' + client_overrides: 'test1/client-overrides/' admin: email: 'admin1@example.com' diff --git a/config/test-2.yaml b/config/test-2.yaml index 82d4aa35f..b559769c3 100644 --- a/config/test-2.yaml +++ b/config/test-2.yaml @@ -22,6 +22,7 @@ storage: captions: 'test2/captions/' cache: 'test2/cache/' plugins: 'test2/plugins/' + client_overrides: 'test2/client-overrides/' admin: email: 'admin2@example.com' diff --git a/config/test-3.yaml b/config/test-3.yaml index d2734f469..9a7a944e9 100644 --- a/config/test-3.yaml +++ b/config/test-3.yaml @@ -22,6 +22,7 @@ storage: captions: 'test3/captions/' cache: 'test3/cache/' plugins: 'test3/plugins/' + client_overrides: 'test3/client-overrides/' admin: email: 'admin3@example.com' diff --git a/config/test-4.yaml b/config/test-4.yaml index 9ec45b024..1e4bee974 100644 --- a/config/test-4.yaml +++ b/config/test-4.yaml @@ -22,6 +22,7 @@ storage: captions: 'test4/captions/' cache: 'test4/cache/' plugins: 'test4/plugins/' + client_overrides: 'test4/client-overrides/' admin: email: 'admin4@example.com' diff --git a/config/test-5.yaml b/config/test-5.yaml index 92cc113b9..9725e84f4 100644 --- a/config/test-5.yaml +++ b/config/test-5.yaml @@ -22,6 +22,7 @@ storage: captions: 'test5/captions/' cache: 'test5/cache/' plugins: 'test5/plugins/' + client_overrides: 'test5/client-overrides/' admin: email: 'admin5@example.com' diff --git a/config/test-6.yaml b/config/test-6.yaml index 205d99797..a04c8a6a9 100644 --- a/config/test-6.yaml +++ b/config/test-6.yaml @@ -22,6 +22,7 @@ storage: captions: 'test6/captions/' cache: 'test6/cache/' plugins: 'test6/plugins/' + client_overrides: 'test6/client-overrides/' admin: email: 'admin6@example.com' diff --git a/server/controllers/client.ts b/server/controllers/client.ts index 65b5a053c..88f51907b 100644 --- a/server/controllers/client.ts +++ b/server/controllers/client.ts @@ -1,3 +1,4 @@ +import { constants, promises as fs } from 'fs' import * as express from 'express' import { join } from 'path' import { root } from '../helpers/core-utils' @@ -39,20 +40,40 @@ clientsRouter.use( ) // Static HTML/CSS/JS client files - const staticClientFiles = [ - 'manifest.webmanifest', 'ngsw-worker.js', 'ngsw.json' ] + for (const staticClientFile of staticClientFiles) { const path = join(root(), 'client', 'dist', staticClientFile) - clientsRouter.get('/' + staticClientFile, (req: express.Request, res: express.Response) => { + clientsRouter.get(`/${staticClientFile}`, (req: express.Request, res: express.Response) => { res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) }) } +// Dynamic PWA manifest +clientsRouter.get('/manifest.webmanifest', asyncMiddleware(generateManifest)) + +// Static client overrides +const staticClientOverrides = [ + 'assets/images/logo.svg', + 'assets/images/favicon.png', + 'assets/images/icons/icon-36x36.png', + 'assets/images/icons/icon-48x48.png', + 'assets/images/icons/icon-72x72.png', + 'assets/images/icons/icon-96x96.png', + 'assets/images/icons/icon-144x144.png', + 'assets/images/icons/icon-192x192.png', + 'assets/images/icons/icon-512x512.png' +] + +for (const staticClientOverride of staticClientOverrides) { + const overridePhysicalPath = join(CONFIG.STORAGE.CLIENT_OVERRIDES_DIR, staticClientOverride) + clientsRouter.use(`/client/${staticClientOverride}`, asyncMiddleware(serveClientOverride(overridePhysicalPath))) +} + clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations) clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT })) @@ -130,3 +151,28 @@ function sendHTML (html: string, res: express.Response) { return res.send(html) } + +async function generateManifest (req: express.Request, res: express.Response) { + const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest') + const manifestJson = await fs.readFile(manifestPhysicalPath, 'utf8') + const manifest = JSON.parse(manifestJson) + + manifest.name = CONFIG.INSTANCE.NAME + manifest.short_name = CONFIG.INSTANCE.NAME + manifest.description = CONFIG.INSTANCE.SHORT_DESCRIPTION + + res.json(manifest) +} + +function serveClientOverride (path: string) { + return async (req: express.Request, res: express.Response, next: express.NextFunction) => { + try { + await fs.access(path, constants.F_OK) + // Serve override client + res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) + } catch { + // Serve dist client + next() + } + } +} diff --git a/server/initializers/config.ts b/server/initializers/config.ts index 48e2cbc1a..32bd3bbe2 100644 --- a/server/initializers/config.ts +++ b/server/initializers/config.ts @@ -68,7 +68,8 @@ const CONFIG = { CAPTIONS_DIR: buildPath(config.get('storage.captions')), TORRENTS_DIR: buildPath(config.get('storage.torrents')), CACHE_DIR: buildPath(config.get('storage.cache')), - PLUGINS_DIR: buildPath(config.get('storage.plugins')) + PLUGINS_DIR: buildPath(config.get('storage.plugins')), + CLIENT_OVERRIDES_DIR: buildPath(config.get('storage.client_overrides')) }, WEBSERVER: { SCHEME: config.get('webserver.https') === true ? 'https' : 'http', diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 9a262fd4b..e730e3c84 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -1,4 +1,5 @@ import { join } from 'path' +import { randomBytes } from 'crypto' import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' import { ActivityPubActorType } from '../../shared/models/activitypub' import { FollowState } from '../../shared/models/actors' @@ -710,6 +711,14 @@ registerConfigChangedHandler(() => { // --------------------------------------------------------------------------- +const FILES_CONTENT_HASH = { + MANIFEST: generateContentHash(), + FAVICON: generateContentHash(), + LOGO: generateContentHash() +} + +// --------------------------------------------------------------------------- + export { WEBSERVER, API_VERSION, @@ -792,8 +801,10 @@ export { VIDEO_PLAYLIST_PRIVACIES, PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME, ASSETS_PATH, + FILES_CONTENT_HASH, loadLanguages, - buildLanguages + buildLanguages, + generateContentHash } // --------------------------------------------------------------------------- @@ -895,3 +906,7 @@ function buildLanguages () { return languages } + +function generateContentHash () { + return randomBytes(20).toString('hex') +} diff --git a/server/lib/client-html.ts b/server/lib/client-html.ts index 3e6da2898..5996f3c70 100644 --- a/server/lib/client-html.ts +++ b/server/lib/client-html.ts @@ -1,6 +1,6 @@ import * as express from 'express' import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' -import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER } from '../initializers/constants' +import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER, FILES_CONTENT_HASH } from '../initializers/constants' import { join } from 'path' import { escapeHTML, sha256 } from '../helpers/core-utils' import { VideoModel } from '../models/video/video' @@ -101,6 +101,9 @@ export class ClientHtml { let html = buffer.toString() if (paramLang) html = ClientHtml.addHtmlLang(html, paramLang) + html = ClientHtml.addManifestContentHash(html) + html = ClientHtml.addFaviconContentHash(html) + html = ClientHtml.addLogoContentHash(html) html = ClientHtml.addCustomCSS(html) html = await ClientHtml.addAsyncPluginCSS(html) @@ -136,6 +139,18 @@ export class ClientHtml { return htmlStringPage.replace('', ``) } + private static addManifestContentHash (htmlStringPage: string) { + return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) + } + + private static addFaviconContentHash(htmlStringPage: string) { + return htmlStringPage.replace('[faviconContentHash]', FILES_CONTENT_HASH.FAVICON) + } + + private static addLogoContentHash(htmlStringPage: string) { + return htmlStringPage.replace('[logoContentHash]', FILES_CONTENT_HASH.LOGO) + } + private static addTitleTag (htmlStringPage: string, title?: string) { let text = title || CONFIG.INSTANCE.NAME if (title) text += ` - ${CONFIG.INSTANCE.NAME}` diff --git a/support/docker/production/config/production.yaml b/support/docker/production/config/production.yaml index 4eeca4110..a32cf1a89 100644 --- a/support/docker/production/config/production.yaml +++ b/support/docker/production/config/production.yaml @@ -54,6 +54,11 @@ storage: captions: '../data/captions/' cache: '../data/cache/' plugins: '../data/plugins/' + # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images + # Could contain for example assets/images/favicon.png + # If the file exists, peertube will serve it + # If not, peertube will fallback to the default fil + client_overrides: '../data/client-overrides/' log: level: 'info' # debug/info/warning/error -- 2.41.0