-
david.bauer authoreddavid.bauer authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
files.js 5.30 KiB
/**
* @copyright Copyright (c) Open-Xchange GmbH, Germany <info@open-xchange.com>
* @license AGPL-3.0
*
* This code is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with OX App Suite. If not, see <https://www.gnu.org/licenses/agpl-3.0.txt>.
*
* Any use of the work other than as authorized under this license or copyright law is prohibited.
*/
import nodePath from 'node:path'
import { promisify } from 'node:util'
import zlib from 'node:zlib'
import * as cache from './cache.js'
import { configMap } from './config_map.js'
import { NotAllowedOriginError, NotFoundError, VersionMismatchError, isVersionMismatchError } from './errors.js'
import logger from './logger.js'
import { getCSSDependenciesFor, getViteManifests } from './manifests.js'
import { getVersionInfo } from './version.js'
const gzip = promisify(zlib.gzip)
const brotliCompress = promisify(zlib.brotliCompress)
const compressFileSize = Number(process.env.COMPRESS_FILE_SIZE)
const compressionMimeTypes = (process.env.COMPRESS_FILE_TYPES || '').replace(/([.+*?^$()[\]{}|])/g, '\\$1').split(' ')
const compressionWhitelistRegex = new RegExp(`^(${compressionMimeTypes.join('|')})($|;)`, 'i')
export async function fetchFileWithHeadersFromBaseUrl ({ path, baseUrl, version }) {
const upstreamUrl = new URL(path, baseUrl)
if (configMap.origins.includes(upstreamUrl.origin) === false) {
logger.debug(`"${upstreamUrl.origin}" does not match valid baseUrl: "${baseUrl}".`)
throw new NotAllowedOriginError('This origin is not allowed', { status: 400 })
}
const [response, dependencies] = await Promise.all([
fetch(upstreamUrl, { cache: 'no-store' }),
nodePath.extname(path) === '.js' && getCSSDependenciesFor({ file: path.substr(1), version })
])
if (!response.ok) {
if (response.status === 404) logger.trace(`[Files] "${path}" could not be found on "${baseUrl}". Responded with: ${response.status}`)
else logger.error(`[Files] Unexpected result from file retrieval "${path}" on "${baseUrl}", responded with: ${response.status}`)
throw new NotFoundError(`Error fetching file: ${path}`, { status: response.status })
}
if (response.headers.get('version')) {
const requestedVersion = (await getVersionInfo()).details[baseUrl]
const receivedVersion = response.headers.get('version')
if (requestedVersion !== receivedVersion) {
throw new VersionMismatchError(`${path} does not contain the right version. Needs ${requestedVersion} but received ${receivedVersion}`)
}
}
const result = {
body: Buffer.from(await response.arrayBuffer()),
headers: {
'content-type': response.headers.get('content-type') === 'application/javascript' ? 'application/javascript; charset=utf-8' : response.headers.get('content-type'),
dependencies
}
}
if (result.body.length > compressFileSize && compressionWhitelistRegex.test(result.headers['content-type'])) {
if (path === '/index.html') {
result.body = await gzip(result.body)
result.headers['content-encoding'] = 'gzip'
} else {
result.body = await brotliCompress(result.body)
result.headers['content-encoding'] = 'br'
}
}
return result
}
export async function fetchFileWithHeaders ({ path, version }) {
const viteManifests = await getViteManifests({ version })
const module = viteManifests[path.substr(1)]
if (module?.meta?.baseUrl) {
const baseUrl = module?.meta?.baseUrl
try {
return fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version })
} catch (err) {
logger.debug(`[Files] File ${path} had a baseUrl but could not be found on that server: ${err}`)
if (isVersionMismatchError(err)) throw err
}
}
return Promise.any(configMap.urls.map(baseUrl => fetchFileWithHeadersFromBaseUrl({ path, baseUrl, version })))
}
export function getFile ({ version, path, fetchFiles = true }) {
return cache.getFile({ name: path, version }, ({ name: path, version }) => {
if (!fetchFiles) throw new Error('[File] Not found in cache: ' + path)
return fetchFileWithHeaders({ version, path }).catch((err) => {
if (!isVersionMismatchError(err)) throw err
logger.warn(`[Files] The file ${path} has been delivered with the wrong version from the UI container.`)
throw err
})
})
}
export async function warmCache ({ version, fetchFiles = false }) {
const start = +new Date()
logger.info('[File] start warming up the cache')
const viteManifests = {}
try {
Object.assign(viteManifests, await getViteManifests({ version }))
} catch (err) {}
const paths = Object.keys(viteManifests).map(key => `/${viteManifests[key].file}`)
while (paths.length > 0) {
await Promise.all(paths.splice(0, 20).map(path => {
return getFile({ version, path, fetchFiles })
}))
}
logger.info(`[File] finished warming up the cache in ${Math.floor((+new Date() - start) / 1000)}s`)
}