From f3f13d72ff436d91752fd338f1eb4f1cde149661 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Julian=20B=C3=A4ume?= <julian.baeume@open-xchange.com>
Date: Tue, 5 Oct 2021 17:04:45 +0200
Subject: [PATCH] proxy files not part of manifest.json files to all involved
 containers

first hit wins. This allows us to transparently use sourcemaps and other files that
are not part of the sources according to the vite manifest
---
 spec/file_caching_test.js | 14 +++++++++++++-
 src/config.js             | 17 +++++++++++++++++
 src/createApp.js          |  8 ++++++++
 src/files.js              | 29 ++++++++++++++++++++---------
 src/manifests.js          |  9 +++------
 5 files changed, 61 insertions(+), 16 deletions(-)
 create mode 100644 src/config.js

diff --git a/spec/file_caching_test.js b/spec/file_caching_test.js
index c545637..cf11011 100644
--- a/spec/file_caching_test.js
+++ b/spec/file_caching_test.js
@@ -41,7 +41,8 @@ describe('File caching service', () => {
       '/test.txt': (req, res) => res.setHeader('content-type', 'text/plain').status(200).send('this is test'),
       '/index.html.js': (req, res) => res.setHeader('content-type', 'application/javascript').status(200).send('this is index.html.js'),
       '/index.html': (req, res) => res.setHeader('content-type', 'text/html').status(200).send('<html><head></head><body>it\'s me</body></html>'),
-      '/main.css': (req, res) => res.setHeader('content-type', 'text/css').status(200).send('.foo { color: #000; }')
+      '/main.css': (req, res) => res.setHeader('content-type', 'text/css').status(200).send('.foo { color: #000; }'),
+      '/favicon.ico': 'not really a favicon, though'
 
     })
     await request(app).get('/ready')
@@ -77,4 +78,15 @@ describe('File caching service', () => {
     expect(response.headers['content-type']).toBe('text/html; charset=utf-8')
     expect(response.text).toBe('<html><head></head><body>it\'s me</body></html>')
   })
+
+  it('directly fetches files not referenced in manifest.json files from the upstream servers', async () => {
+    const response = await request(app).get('/favicon.ico')
+    expect(response.statusCode).toBe(200)
+    expect(response.body).toBe('not really a favicon, though')
+  })
+
+  it('returns 404 if file can not be resolved', async () => {
+    const response = await request(app).get('/unknown-file.txt')
+    expect(response.statusCode).toBe(404)
+  })
 })
diff --git a/src/config.js b/src/config.js
new file mode 100644
index 0000000..2747ac6
--- /dev/null
+++ b/src/config.js
@@ -0,0 +1,17 @@
+import fs from 'fs/promises'
+import yaml from 'js-yaml'
+
+class Config {
+  async load () {
+    const urlsSource = await fs.readFile('./config/manifests/urls.yaml', 'utf8')
+    this._urls = yaml.load(urlsSource).manifests
+  }
+
+  get urls () {
+    return this._urls || []
+  }
+}
+
+export const config = new Config()
+
+export default config
diff --git a/src/createApp.js b/src/createApp.js
index f68d9b3..ef28280 100644
--- a/src/createApp.js
+++ b/src/createApp.js
@@ -90,6 +90,14 @@ export function createApp () {
     next()
   })
 
+  app.use(async (req, res, next) => {
+    try {
+      const { 'content-type': contentType, content } = await fileCache.fetchAndStore(req.path)
+      if (content) return res.setHeader('content-type', contentType).status(200).send(content)
+    } catch (e) {}
+    next()
+  })
+
   app.use(function (err, req, res, next) {
     logger.error(err)
     res.status(500).end()
diff --git a/src/files.js b/src/files.js
index b059dc3..6bc4d9c 100644
--- a/src/files.js
+++ b/src/files.js
@@ -1,6 +1,18 @@
 import fetch from 'node-fetch'
 import crypto from 'crypto'
+import { config } from './config.js'
 
+async function fetchData (path, baseUrl) {
+  const response = await fetch(new URL(path, baseUrl))
+  if (!response.ok) return null
+  const content = await response.buffer()
+  const sha256Sum = crypto.createHash('sha256').update(content).digest('base64')
+  return [path, {
+    'content-type': response.headers.get('content-type'),
+    sha256Sum,
+    content
+  }]
+}
 class FileCache {
   constructor () {
     this._cache = {}
@@ -23,15 +35,7 @@ class FileCache {
               console.error('could not find manifest for', file)
               return null
             }
-            const response = await fetch(new URL(file, manifest.meta.baseUrl))
-            if (!response.ok) return null
-            const content = await response.buffer()
-            const sha256Sum = crypto.createHash('sha256').update(content).digest('base64')
-            return [file, {
-              'content-type': response.headers.get('content-type'),
-              sha256Sum,
-              content
-            }]
+            return fetchData(file, manifest.meta.baseUrl)
           } catch (e) { console.error(e) }
         }))).filter(data => Array.isArray(data) && data.length === 2))
       }
@@ -40,6 +44,13 @@ class FileCache {
     this._cache = cache
   }
 
+  async fetchAndStore (path) {
+    if (config.urls.length === 0) await config.load()
+    const [key, value] = await Promise.race(config.urls.map(baseUrl => fetchData(path, baseUrl)))
+    this._cache[key] = value
+    return value
+  }
+
   get (path) {
     return this?._cache[path.slice(1)] || {}
   }
diff --git a/src/manifests.js b/src/manifests.js
index 44b9250..a043e27 100644
--- a/src/manifests.js
+++ b/src/manifests.js
@@ -1,9 +1,8 @@
-import fs from 'fs/promises'
-import yaml from 'js-yaml'
 import fetch from 'node-fetch'
 import path from 'path'
 import { URL } from 'url'
 import { fileCache } from './files.js'
+import { config } from './config.js'
 
 export const loadViteManifests = (() => {
   let cache
@@ -13,12 +12,10 @@ export const loadViteManifests = (() => {
     const CACHE_TTL = parseInt(process.env.CACHE_TTL)
 
     if (!cache || useCache === false || +new Date() > lastCacheTime + CACHE_TTL) {
-      const urlsSource = await fs.readFile('./config/manifests/urls.yaml', 'utf8')
-      const urls = yaml.load(urlsSource).manifests
-
+      await config.load()
       // vite manifests contains a set of objects with the vite-manifests
       // from the corresponding registered services
-      const viteManifests = await Promise.all(urls.map(async url => {
+      const viteManifests = await Promise.all(config.urls.map(async url => {
         const { origin } = new URL(url)
         // fetch the manifests
         const result = await fetch(url)
-- 
GitLab