diff options
author | Kim <1877318+kimsible@users.noreply.github.com> | 2020-07-10 10:20:11 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-07-10 10:20:11 +0200 |
commit | caf2aaf4f9d38ad441a5562c3b8720f8779d6f78 (patch) | |
tree | 2a93cbfd2aee6089fd334f28dcb47c18a368a6fd | |
parent | 27647da17fe53ff24ed27ef8618bc244c0be6b26 (diff) | |
download | PeerTube-caf2aaf4f9d38ad441a5562c3b8720f8779d6f78.tar.gz PeerTube-caf2aaf4f9d38ad441a5562c3b8720f8779d6f78.tar.zst PeerTube-caf2aaf4f9d38ad441a5562c3b8720f8779d6f78.zip |
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 <kimsible@users.noreply.github.com>
-rw-r--r-- | client/src/app/app.component.scss | 2 | ||||
-rw-r--r-- | client/src/index.html | 11 | ||||
-rw-r--r-- | config/default.yaml | 5 | ||||
-rw-r--r-- | config/production.yaml.example | 5 | ||||
-rw-r--r-- | config/test-1.yaml | 1 | ||||
-rw-r--r-- | config/test-2.yaml | 1 | ||||
-rw-r--r-- | config/test-3.yaml | 1 | ||||
-rw-r--r-- | config/test-4.yaml | 1 | ||||
-rw-r--r-- | config/test-5.yaml | 1 | ||||
-rw-r--r-- | config/test-6.yaml | 1 | ||||
-rw-r--r-- | server/controllers/client.ts | 52 | ||||
-rw-r--r-- | server/initializers/config.ts | 3 | ||||
-rw-r--r-- | server/initializers/constants.ts | 17 | ||||
-rw-r--r-- | server/lib/client-html.ts | 17 | ||||
-rw-r--r-- | support/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 @@ | |||
62 | 62 | ||
63 | .icon.icon-logo { | 63 | .icon.icon-logo { |
64 | display: inline-block; | 64 | display: inline-block; |
65 | background: url('../assets/images/logo.svg') no-repeat; | 65 | background-repeat: no-repeat; |
66 | width: 23px; | 66 | width: 23px; |
67 | height: 24px; | 67 | height: 24px; |
68 | margin-right: .5rem; | 68 | 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 @@ | |||
7 | <meta name="theme-color" content="#fff" /> | 7 | <meta name="theme-color" content="#fff" /> |
8 | <meta property="og:platform" content="PeerTube" /> | 8 | <meta property="og:platform" content="PeerTube" /> |
9 | <!-- Web Manifest file --> | 9 | <!-- Web Manifest file --> |
10 | <link rel="manifest" href="/manifest.webmanifest"> | 10 | <link rel="manifest" href="/manifest.webmanifest?[manifestContentHash]"> |
11 | 11 | ||
12 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png" /> | 12 | <link rel="icon" type="image/png" href="/client/assets/images/favicon.png?[faviconContentHash]" /> |
13 | |||
14 | <!-- logo background-image --> | ||
15 | <style type="text/css"> | ||
16 | .icon-logo { | ||
17 | background-image: url(/client/assets/images/logo.svg?[logoContentHash]); | ||
18 | } | ||
19 | </style> | ||
13 | 20 | ||
14 | <!-- base url --> | 21 | <!-- base url --> |
15 | <base href="/"> | 22 | <base href="/"> |
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: | |||
85 | captions: 'storage/captions/' | 85 | captions: 'storage/captions/' |
86 | cache: 'storage/cache/' | 86 | cache: 'storage/cache/' |
87 | plugins: 'storage/plugins/' | 87 | plugins: 'storage/plugins/' |
88 | # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images | ||
89 | # Could contain for example assets/images/favicon.png | ||
90 | # If the file exists, peertube will serve it | ||
91 | # If not, peertube will fallback to the default fil | ||
92 | client_overrides: 'storage/client-overrides/' | ||
88 | 93 | ||
89 | log: | 94 | log: |
90 | level: 'info' # debug/info/warning/error | 95 | 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: | |||
86 | captions: '/var/www/peertube/storage/captions/' | 86 | captions: '/var/www/peertube/storage/captions/' |
87 | cache: '/var/www/peertube/storage/cache/' | 87 | cache: '/var/www/peertube/storage/cache/' |
88 | plugins: '/var/www/peertube/storage/plugins/' | 88 | plugins: '/var/www/peertube/storage/plugins/' |
89 | # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images | ||
90 | # Could contain for example assets/images/favicon.png | ||
91 | # If the file exists, peertube will serve it | ||
92 | # If not, peertube will fallback to the default fil | ||
93 | client_overrides: '/var/www/peertube/storage/client-overrides/' | ||
89 | 94 | ||
90 | log: | 95 | log: |
91 | level: 'info' # debug/info/warning/error | 96 | 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: | |||
22 | captions: 'test1/captions/' | 22 | captions: 'test1/captions/' |
23 | cache: 'test1/cache/' | 23 | cache: 'test1/cache/' |
24 | plugins: 'test1/plugins/' | 24 | plugins: 'test1/plugins/' |
25 | client_overrides: 'test1/client-overrides/' | ||
25 | 26 | ||
26 | admin: | 27 | admin: |
27 | email: 'admin1@example.com' | 28 | 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: | |||
22 | captions: 'test2/captions/' | 22 | captions: 'test2/captions/' |
23 | cache: 'test2/cache/' | 23 | cache: 'test2/cache/' |
24 | plugins: 'test2/plugins/' | 24 | plugins: 'test2/plugins/' |
25 | client_overrides: 'test2/client-overrides/' | ||
25 | 26 | ||
26 | admin: | 27 | admin: |
27 | email: 'admin2@example.com' | 28 | 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: | |||
22 | captions: 'test3/captions/' | 22 | captions: 'test3/captions/' |
23 | cache: 'test3/cache/' | 23 | cache: 'test3/cache/' |
24 | plugins: 'test3/plugins/' | 24 | plugins: 'test3/plugins/' |
25 | client_overrides: 'test3/client-overrides/' | ||
25 | 26 | ||
26 | admin: | 27 | admin: |
27 | email: 'admin3@example.com' | 28 | 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: | |||
22 | captions: 'test4/captions/' | 22 | captions: 'test4/captions/' |
23 | cache: 'test4/cache/' | 23 | cache: 'test4/cache/' |
24 | plugins: 'test4/plugins/' | 24 | plugins: 'test4/plugins/' |
25 | client_overrides: 'test4/client-overrides/' | ||
25 | 26 | ||
26 | admin: | 27 | admin: |
27 | email: 'admin4@example.com' | 28 | 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: | |||
22 | captions: 'test5/captions/' | 22 | captions: 'test5/captions/' |
23 | cache: 'test5/cache/' | 23 | cache: 'test5/cache/' |
24 | plugins: 'test5/plugins/' | 24 | plugins: 'test5/plugins/' |
25 | client_overrides: 'test5/client-overrides/' | ||
25 | 26 | ||
26 | admin: | 27 | admin: |
27 | email: 'admin5@example.com' | 28 | 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: | |||
22 | captions: 'test6/captions/' | 22 | captions: 'test6/captions/' |
23 | cache: 'test6/cache/' | 23 | cache: 'test6/cache/' |
24 | plugins: 'test6/plugins/' | 24 | plugins: 'test6/plugins/' |
25 | client_overrides: 'test6/client-overrides/' | ||
25 | 26 | ||
26 | admin: | 27 | admin: |
27 | email: 'admin6@example.com' | 28 | 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 @@ | |||
1 | import { constants, promises as fs } from 'fs' | ||
1 | import * as express from 'express' | 2 | import * as express from 'express' |
2 | import { join } from 'path' | 3 | import { join } from 'path' |
3 | import { root } from '../helpers/core-utils' | 4 | import { root } from '../helpers/core-utils' |
@@ -39,20 +40,40 @@ clientsRouter.use( | |||
39 | ) | 40 | ) |
40 | 41 | ||
41 | // Static HTML/CSS/JS client files | 42 | // Static HTML/CSS/JS client files |
42 | |||
43 | const staticClientFiles = [ | 43 | const staticClientFiles = [ |
44 | 'manifest.webmanifest', | ||
45 | 'ngsw-worker.js', | 44 | 'ngsw-worker.js', |
46 | 'ngsw.json' | 45 | 'ngsw.json' |
47 | ] | 46 | ] |
47 | |||
48 | for (const staticClientFile of staticClientFiles) { | 48 | for (const staticClientFile of staticClientFiles) { |
49 | const path = join(root(), 'client', 'dist', staticClientFile) | 49 | const path = join(root(), 'client', 'dist', staticClientFile) |
50 | 50 | ||
51 | clientsRouter.get('/' + staticClientFile, (req: express.Request, res: express.Response) => { | 51 | clientsRouter.get(`/${staticClientFile}`, (req: express.Request, res: express.Response) => { |
52 | res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) | 52 | res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) |
53 | }) | 53 | }) |
54 | } | 54 | } |
55 | 55 | ||
56 | // Dynamic PWA manifest | ||
57 | clientsRouter.get('/manifest.webmanifest', asyncMiddleware(generateManifest)) | ||
58 | |||
59 | // Static client overrides | ||
60 | const staticClientOverrides = [ | ||
61 | 'assets/images/logo.svg', | ||
62 | 'assets/images/favicon.png', | ||
63 | 'assets/images/icons/icon-36x36.png', | ||
64 | 'assets/images/icons/icon-48x48.png', | ||
65 | 'assets/images/icons/icon-72x72.png', | ||
66 | 'assets/images/icons/icon-96x96.png', | ||
67 | 'assets/images/icons/icon-144x144.png', | ||
68 | 'assets/images/icons/icon-192x192.png', | ||
69 | 'assets/images/icons/icon-512x512.png' | ||
70 | ] | ||
71 | |||
72 | for (const staticClientOverride of staticClientOverrides) { | ||
73 | const overridePhysicalPath = join(CONFIG.STORAGE.CLIENT_OVERRIDES_DIR, staticClientOverride) | ||
74 | clientsRouter.use(`/client/${staticClientOverride}`, asyncMiddleware(serveClientOverride(overridePhysicalPath))) | ||
75 | } | ||
76 | |||
56 | clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations) | 77 | clientsRouter.use('/client/locales/:locale/:file.json', serveServerTranslations) |
57 | clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT })) | 78 | clientsRouter.use('/client', express.static(distPath, { maxAge: STATIC_MAX_AGE.CLIENT })) |
58 | 79 | ||
@@ -130,3 +151,28 @@ function sendHTML (html: string, res: express.Response) { | |||
130 | 151 | ||
131 | return res.send(html) | 152 | return res.send(html) |
132 | } | 153 | } |
154 | |||
155 | async function generateManifest (req: express.Request, res: express.Response) { | ||
156 | const manifestPhysicalPath = join(root(), 'client', 'dist', 'manifest.webmanifest') | ||
157 | const manifestJson = await fs.readFile(manifestPhysicalPath, 'utf8') | ||
158 | const manifest = JSON.parse(manifestJson) | ||
159 | |||
160 | manifest.name = CONFIG.INSTANCE.NAME | ||
161 | manifest.short_name = CONFIG.INSTANCE.NAME | ||
162 | manifest.description = CONFIG.INSTANCE.SHORT_DESCRIPTION | ||
163 | |||
164 | res.json(manifest) | ||
165 | } | ||
166 | |||
167 | function serveClientOverride (path: string) { | ||
168 | return async (req: express.Request, res: express.Response, next: express.NextFunction) => { | ||
169 | try { | ||
170 | await fs.access(path, constants.F_OK) | ||
171 | // Serve override client | ||
172 | res.sendFile(path, { maxAge: STATIC_MAX_AGE.SERVER }) | ||
173 | } catch { | ||
174 | // Serve dist client | ||
175 | next() | ||
176 | } | ||
177 | } | ||
178 | } | ||
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 = { | |||
68 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), | 68 | CAPTIONS_DIR: buildPath(config.get<string>('storage.captions')), |
69 | TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')), | 69 | TORRENTS_DIR: buildPath(config.get<string>('storage.torrents')), |
70 | CACHE_DIR: buildPath(config.get<string>('storage.cache')), | 70 | CACHE_DIR: buildPath(config.get<string>('storage.cache')), |
71 | PLUGINS_DIR: buildPath(config.get<string>('storage.plugins')) | 71 | PLUGINS_DIR: buildPath(config.get<string>('storage.plugins')), |
72 | CLIENT_OVERRIDES_DIR: buildPath(config.get<string>('storage.client_overrides')) | ||
72 | }, | 73 | }, |
73 | WEBSERVER: { | 74 | WEBSERVER: { |
74 | SCHEME: config.get<boolean>('webserver.https') === true ? 'https' : 'http', | 75 | SCHEME: config.get<boolean>('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 @@ | |||
1 | import { join } from 'path' | 1 | import { join } from 'path' |
2 | import { randomBytes } from 'crypto' | ||
2 | import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' | 3 | import { JobType, VideoRateType, VideoResolution, VideoState } from '../../shared/models' |
3 | import { ActivityPubActorType } from '../../shared/models/activitypub' | 4 | import { ActivityPubActorType } from '../../shared/models/activitypub' |
4 | import { FollowState } from '../../shared/models/actors' | 5 | import { FollowState } from '../../shared/models/actors' |
@@ -710,6 +711,14 @@ registerConfigChangedHandler(() => { | |||
710 | 711 | ||
711 | // --------------------------------------------------------------------------- | 712 | // --------------------------------------------------------------------------- |
712 | 713 | ||
714 | const FILES_CONTENT_HASH = { | ||
715 | MANIFEST: generateContentHash(), | ||
716 | FAVICON: generateContentHash(), | ||
717 | LOGO: generateContentHash() | ||
718 | } | ||
719 | |||
720 | // --------------------------------------------------------------------------- | ||
721 | |||
713 | export { | 722 | export { |
714 | WEBSERVER, | 723 | WEBSERVER, |
715 | API_VERSION, | 724 | API_VERSION, |
@@ -792,8 +801,10 @@ export { | |||
792 | VIDEO_PLAYLIST_PRIVACIES, | 801 | VIDEO_PLAYLIST_PRIVACIES, |
793 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME, | 802 | PLUGIN_EXTERNAL_AUTH_TOKEN_LIFETIME, |
794 | ASSETS_PATH, | 803 | ASSETS_PATH, |
804 | FILES_CONTENT_HASH, | ||
795 | loadLanguages, | 805 | loadLanguages, |
796 | buildLanguages | 806 | buildLanguages, |
807 | generateContentHash | ||
797 | } | 808 | } |
798 | 809 | ||
799 | // --------------------------------------------------------------------------- | 810 | // --------------------------------------------------------------------------- |
@@ -895,3 +906,7 @@ function buildLanguages () { | |||
895 | 906 | ||
896 | return languages | 907 | return languages |
897 | } | 908 | } |
909 | |||
910 | function generateContentHash () { | ||
911 | return randomBytes(20).toString('hex') | ||
912 | } | ||
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 @@ | |||
1 | import * as express from 'express' | 1 | import * as express from 'express' |
2 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' | 2 | import { buildFileLocale, getDefaultLocale, is18nLocale, POSSIBLE_LOCALES } from '../../shared/models/i18n/i18n' |
3 | import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER } from '../initializers/constants' | 3 | import { CUSTOM_HTML_TAG_COMMENTS, EMBED_SIZE, PLUGIN_GLOBAL_CSS_PATH, WEBSERVER, FILES_CONTENT_HASH } from '../initializers/constants' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { escapeHTML, sha256 } from '../helpers/core-utils' | 5 | import { escapeHTML, sha256 } from '../helpers/core-utils' |
6 | import { VideoModel } from '../models/video/video' | 6 | import { VideoModel } from '../models/video/video' |
@@ -101,6 +101,9 @@ export class ClientHtml { | |||
101 | let html = buffer.toString() | 101 | let html = buffer.toString() |
102 | 102 | ||
103 | if (paramLang) html = ClientHtml.addHtmlLang(html, paramLang) | 103 | if (paramLang) html = ClientHtml.addHtmlLang(html, paramLang) |
104 | html = ClientHtml.addManifestContentHash(html) | ||
105 | html = ClientHtml.addFaviconContentHash(html) | ||
106 | html = ClientHtml.addLogoContentHash(html) | ||
104 | html = ClientHtml.addCustomCSS(html) | 107 | html = ClientHtml.addCustomCSS(html) |
105 | html = await ClientHtml.addAsyncPluginCSS(html) | 108 | html = await ClientHtml.addAsyncPluginCSS(html) |
106 | 109 | ||
@@ -136,6 +139,18 @@ export class ClientHtml { | |||
136 | return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`) | 139 | return htmlStringPage.replace('<html>', `<html lang="${paramLang}">`) |
137 | } | 140 | } |
138 | 141 | ||
142 | private static addManifestContentHash (htmlStringPage: string) { | ||
143 | return htmlStringPage.replace('[manifestContentHash]', FILES_CONTENT_HASH.MANIFEST) | ||
144 | } | ||
145 | |||
146 | private static addFaviconContentHash(htmlStringPage: string) { | ||
147 | return htmlStringPage.replace('[faviconContentHash]', FILES_CONTENT_HASH.FAVICON) | ||
148 | } | ||
149 | |||
150 | private static addLogoContentHash(htmlStringPage: string) { | ||
151 | return htmlStringPage.replace('[logoContentHash]', FILES_CONTENT_HASH.LOGO) | ||
152 | } | ||
153 | |||
139 | private static addTitleTag (htmlStringPage: string, title?: string) { | 154 | private static addTitleTag (htmlStringPage: string, title?: string) { |
140 | let text = title || CONFIG.INSTANCE.NAME | 155 | let text = title || CONFIG.INSTANCE.NAME |
141 | if (title) text += ` - ${CONFIG.INSTANCE.NAME}` | 156 | 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: | |||
54 | captions: '../data/captions/' | 54 | captions: '../data/captions/' |
55 | cache: '../data/cache/' | 55 | cache: '../data/cache/' |
56 | plugins: '../data/plugins/' | 56 | plugins: '../data/plugins/' |
57 | # Overridable client files : logo.svg, favicon.png and icons/*.png (PWA) in client/dist/assets/images | ||
58 | # Could contain for example assets/images/favicon.png | ||
59 | # If the file exists, peertube will serve it | ||
60 | # If not, peertube will fallback to the default fil | ||
61 | client_overrides: '../data/client-overrides/' | ||
57 | 62 | ||
58 | log: | 63 | log: |
59 | level: 'info' # debug/info/warning/error | 64 | level: 'info' # debug/info/warning/error |