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