From 6841660ba4ac6b93e94cde43acc925ffab49194c Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 12:01:46 +0100 Subject: [PATCH 01/10] app-backend: added initial db migration for asset cache Signed-off-by: Patrik Oldsberg --- .../migrations/20211229105307_init.js | 47 +++++++++++++++++++ plugins/app-backend/package.json | 2 + 2 files changed, 49 insertions(+) create mode 100644 plugins/app-backend/migrations/20211229105307_init.js diff --git a/plugins/app-backend/migrations/20211229105307_init.js b/plugins/app-backend/migrations/20211229105307_init.js new file mode 100644 index 0000000000..e44e58f539 --- /dev/null +++ b/plugins/app-backend/migrations/20211229105307_init.js @@ -0,0 +1,47 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + return knex.schema.createTable('static_asset_cache', table => { + table.comment( + 'A cache of static assets that where previously deployed and may still be lazy-loaded by clients', + ); + table + .string('path') + .primary() + .notNullable() + .comment('The path of the file'); + table + .dateTime('last_modified_at') + .notNullable() + .comment( + 'Timestamp of when the asset was most recently seen in a deployment', + ); + table.binary('content').notNullable().comment('The asset content'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + return knex.schema.dropTable('static_asset_cache'); +}; diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index d5cbe9c07d..e8cd4a4135 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -39,6 +39,7 @@ "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", "helmet": "^4.0.0", + "knex": "^0.95.1", "winston": "^3.2.1", "yn": "^4.0.0" }, @@ -51,6 +52,7 @@ }, "files": [ "dist", + "migrations/**/*.{js,d.ts}", "static" ] } From a9dcace703f2487eaa17870a84b13e4969f28a17 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 14:41:51 +0100 Subject: [PATCH 02/10] app-backend: add StaticAssetsStore Signed-off-by: Patrik Oldsberg --- .../migrations/20211229105307_init.js | 15 +- plugins/app-backend/package.json | 3 + .../src/lib/assets/StaticAssetsStore.test.ts | 160 ++++++++++++++++++ .../src/lib/assets/StaticAssetsStore.ts | 152 +++++++++++++++++ plugins/app-backend/src/lib/assets/index.ts | 18 ++ 5 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts create mode 100644 plugins/app-backend/src/lib/assets/StaticAssetsStore.ts create mode 100644 plugins/app-backend/src/lib/assets/index.ts diff --git a/plugins/app-backend/migrations/20211229105307_init.js b/plugins/app-backend/migrations/20211229105307_init.js index e44e58f539..29e37b35f7 100644 --- a/plugins/app-backend/migrations/20211229105307_init.js +++ b/plugins/app-backend/migrations/20211229105307_init.js @@ -20,22 +20,20 @@ * @param {import('knex').Knex} knex */ exports.up = async function up(knex) { - return knex.schema.createTable('static_asset_cache', table => { + return knex.schema.createTable('static_assets_cache', table => { table.comment( 'A cache of static assets that where previously deployed and may still be lazy-loaded by clients', ); - table - .string('path') - .primary() - .notNullable() - .comment('The path of the file'); + table.text('path').primary().notNullable().comment('The path of the file'); table .dateTime('last_modified_at') + .defaultTo(knex.fn.now()) .notNullable() .comment( 'Timestamp of when the asset was most recently seen in a deployment', ); table.binary('content').notNullable().comment('The asset content'); + table.index('last_modified_at', 'static_asset_cache_last_modified_at_idx'); }); }; @@ -43,5 +41,8 @@ exports.up = async function up(knex) { * @param {import('knex').Knex} knex */ exports.down = async function down(knex) { - return knex.schema.dropTable('static_asset_cache'); + await knex.schema.alterTable('static_assets_cache', table => { + table.dropIndex([], 'static_asset_cache_last_modified_at_idx'); + }); + await knex.schema.dropTable('static_assets_cache'); }; diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index e8cd4a4135..77b64b96d1 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -40,10 +40,13 @@ "fs-extra": "9.1.0", "helmet": "^4.0.0", "knex": "^0.95.1", + "lodash": "^4.17.21", + "luxon": "^2.0.2", "winston": "^3.2.1", "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "^0.1.13", "@backstage/cli": "^0.11.0", "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts new file mode 100644 index 0000000000..0230486444 --- /dev/null +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.test.ts @@ -0,0 +1,160 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import { TestDatabases } from '@backstage/backend-test-utils'; +import { StaticAssetsStore } from './StaticAssetsStore'; + +const logger = getVoidLogger(); + +describe('StaticAssetsStore', () => { + const databases = TestDatabases.create({ + ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + it.each(databases.eachSupportedId())( + 'should store and retrieve assets, %p', + async databaseId => { + const store = await StaticAssetsStore.create({ + logger, + database: await databases.init(databaseId), + }); + + await store.storeAssets([ + { + path: 'foo.txt', + content: async () => Buffer.from('foo'), + }, + { + path: 'dir/bar.txt', + content: async () => Buffer.from('bar'), + }, + ]); + + const now = new Date().getTime(); + + const foo = await store.getAsset('foo.txt'); + expect(foo!.path).toBe('foo.txt'); + expect(foo!.lastModifiedAt.getTime()).toBeGreaterThan(now - 5000); + expect(foo!.lastModifiedAt.getTime()).toBeLessThan(now + 5000); + expect(foo!.content).toEqual(Buffer.from('foo')); + + const bar = await store.getAsset('dir/bar.txt'); + expect(bar!.path).toBe('dir/bar.txt'); + expect( + Math.abs(bar!.lastModifiedAt.getTime() - foo!.lastModifiedAt.getTime()), + ).toBeLessThan(1000); + expect(bar!.content).toEqual(Buffer.from('bar')); + + await expect( + store.getAsset('does-not-exist.txt'), + ).resolves.toBeUndefined(); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'should update assets timestamps, but not contents, %p', + async databaseId => { + const store = await StaticAssetsStore.create({ + logger, + database: await databases.init(databaseId), + }); + + await store.storeAssets([ + { + path: 'foo', + content: async () => Buffer.from('foo'), + }, + { + path: 'bar', + content: async () => Buffer.from('bar'), + }, + ]); + + const oldFoo = await store.getAsset('foo'); + expect(oldFoo?.lastModifiedAt).toBeDefined(); + + const oldBar = await store.getAsset('bar'); + expect(oldBar?.lastModifiedAt).toBeDefined(); + + // SQLite dates end up with second precision, so make sure we wait at least 1s + await new Promise(resolve => setTimeout(resolve, 1500)); + + await store.storeAssets([ + { + path: 'foo', + content: async () => Buffer.from('newFoo'), + }, + ]); + + const newFoo = await store.getAsset('foo'); + expect(oldFoo!.lastModifiedAt).not.toEqual(newFoo!.lastModifiedAt); + expect(oldFoo!.lastModifiedAt.getTime()).toBeLessThan( + newFoo!.lastModifiedAt.getTime(), + ); + + // The "static" in "StaticAssetsStore" means that assets aren't allowed to change + expect(newFoo!.content).toEqual(Buffer.from('foo')); + + const sameBar = await store.getAsset('bar'); + expect(oldBar!.lastModifiedAt).toEqual(sameBar!.lastModifiedAt); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'should trim old assets, %p', + async databaseId => { + const database = await databases.init(databaseId); + const store = await StaticAssetsStore.create({ + logger, + database, + }); + + await store.storeAssets([ + { + path: 'new', + content: async () => Buffer.alloc(0), + }, + { + path: 'old', + content: async () => Buffer.alloc(0), + }, + ]); + + // Rewrite modified time of "old" to be 1h in the past + const updated = await database('static_assets_cache') + .where({ path: 'old' }) + .update({ + last_modified_at: + database.client.config.client === 'sqlite3' + ? database.raw(`datetime('now', '-3600 seconds')`) + : database.raw(`now() + interval '-3600 seconds'`), + }); + expect(updated).toBe(1); + + await expect(store.getAsset('new')).resolves.toBeDefined(); + await expect(store.getAsset('old')).resolves.toBeDefined(); + + await store.trimAssets({ maxAgeSeconds: 1800 }); + + await expect(store.getAsset('new')).resolves.toBeDefined(); + await expect(store.getAsset('old')).resolves.toBeUndefined(); + }, + 60_000, + ); +}); diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts new file mode 100644 index 0000000000..7516da7116 --- /dev/null +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts @@ -0,0 +1,152 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { Logger } from 'winston'; +import { DateTime } from 'luxon'; +import partition from 'lodash/partition'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-app-backend', + 'migrations', +); + +export interface StaticAssetInput { + path: string; + content(): Promise; +} + +export interface StaticAsset { + path: string; + content: Buffer; + lastModifiedAt: Date; +} + +interface StaticAssetRow { + path: string; + content: Buffer; + last_modified_at: Date; +} + +/** @internal */ +export interface StaticAssetsStoreOptions { + database: Knex; + logger: Logger; +} + +/** + * A storage for static assets that are assumed to be immutable. + * + * @internal + */ +export class StaticAssetsStore { + #db: Knex; + #logger: Logger; + + static async create(options: StaticAssetsStoreOptions) { + await options.database.migrate.latest({ + directory: migrationsDir, + }); + return new StaticAssetsStore(options); + } + + private constructor(options: StaticAssetsStoreOptions) { + this.#db = options.database; + this.#logger = options.logger; + } + + /** + * Store the provided assets. + * + * If an asset for a given path already exists the modification time will be + * updated, but the contents will not. + */ + async storeAssets(assets: StaticAssetInput[]) { + const existingRows = await this.#db( + 'static_assets_cache', + ).whereIn( + 'path', + assets.map(a => a.path), + ); + const existingAssetPaths = new Set(existingRows.map(r => r.path)); + + const [modified, added] = partition(assets, asset => + existingAssetPaths.has(asset.path), + ); + + this.#logger.info( + `Storing ${modified.length} updated assets and ${added.length} new assets`, + ); + + await this.#db('static_assets_cache') + .update({ + last_modified_at: this.#db.fn.now(), + }) + .whereIn( + 'path', + modified.map(a => a.path), + ); + + for (const asset of added) { + // We ignore conflicts with other nodes, it doesn't matter if someone else + // added the same asset just before us. + await this.#db('static_assets_cache') + .insert({ + path: asset.path, + content: await asset.content(), + }) + .onConflict('path') + .ignore(); + } + } + + /** + * Retrieve an asset from the store with the given path. + */ + async getAsset(path: string): Promise { + const [row] = await this.#db('static_assets_cache').where({ + path, + }); + if (!row) { + return undefined; + } + return { + path: row.path, + content: row.content, + lastModifiedAt: + typeof row.last_modified_at === 'string' + ? DateTime.fromSQL(row.last_modified_at, { zone: 'UTC' }).toJSDate() + : row.last_modified_at, + }; + } + + /** + * Delete any assets from the store whose modification time is older than the max age. + */ + async trimAssets(options: { maxAgeSeconds: number }) { + const { maxAgeSeconds } = options; + await this.#db('static_assets_cache') + .where( + 'last_modified_at', + '<=', + this.#db.client.config.client === 'sqlite3' + ? this.#db.raw(`datetime('now', ?)`, [`-${maxAgeSeconds} seconds`]) + : this.#db.raw(`now() + interval '${-maxAgeSeconds} seconds'`), + ) + .delete(); + } +} diff --git a/plugins/app-backend/src/lib/assets/index.ts b/plugins/app-backend/src/lib/assets/index.ts new file mode 100644 index 0000000000..0e1e495872 --- /dev/null +++ b/plugins/app-backend/src/lib/assets/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { StaticAssetsStore } from './StaticAssetsStore'; +export type { StaticAsset, StaticAssetInput } from './StaticAssetsStore'; From 54ecf37c1503a5d96638f6198844452a4fa7c585 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 15:34:45 +0100 Subject: [PATCH 03/10] app-backend: added utility to find assets to store Signed-off-by: Patrik Oldsberg --- plugins/app-backend/package.json | 1 + .../src/lib/assets/StaticAssetsStore.ts | 12 +------ .../src/lib/assets/findStaticAssets.ts | 35 +++++++++++++++++++ plugins/app-backend/src/lib/assets/index.ts | 3 +- plugins/app-backend/src/lib/assets/types.ts | 26 ++++++++++++++ 5 files changed, 65 insertions(+), 12 deletions(-) create mode 100644 plugins/app-backend/src/lib/assets/findStaticAssets.ts create mode 100644 plugins/app-backend/src/lib/assets/types.ts diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 77b64b96d1..4e682851bf 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -35,6 +35,7 @@ "@backstage/config": "^0.1.12", "@backstage/types": "^0.1.1", "@types/express": "^4.17.6", + "globby": "^11.0.0", "express": "^4.17.1", "express-promise-router": "^4.1.0", "fs-extra": "9.1.0", diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts index 7516da7116..35a5171c83 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts @@ -19,23 +19,13 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; import { DateTime } from 'luxon'; import partition from 'lodash/partition'; +import { StaticAsset, StaticAssetInput } from './types'; const migrationsDir = resolvePackagePath( '@backstage/plugin-app-backend', 'migrations', ); -export interface StaticAssetInput { - path: string; - content(): Promise; -} - -export interface StaticAsset { - path: string; - content: Buffer; - lastModifiedAt: Date; -} - interface StaticAssetRow { path: string; content: Buffer; diff --git a/plugins/app-backend/src/lib/assets/findStaticAssets.ts b/plugins/app-backend/src/lib/assets/findStaticAssets.ts new file mode 100644 index 0000000000..1e4ba83982 --- /dev/null +++ b/plugins/app-backend/src/lib/assets/findStaticAssets.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; +import globby from 'globby'; +import { StaticAssetInput } from './types'; +import { resolveSafeChildPath } from '@backstage/backend-common'; + +export async function findStaticAssets( + staticDir: string, +): Promise { + const assetPaths = await globby('**/*', { + ignore: ['**/*.map'], // Ignore source maps since they're quite large + cwd: staticDir, + dot: true, + }); + + return assetPaths.map(path => ({ + path, + content: async () => fs.readFile(resolveSafeChildPath(staticDir, path)), + })); +} diff --git a/plugins/app-backend/src/lib/assets/index.ts b/plugins/app-backend/src/lib/assets/index.ts index 0e1e495872..c8cb838dd6 100644 --- a/plugins/app-backend/src/lib/assets/index.ts +++ b/plugins/app-backend/src/lib/assets/index.ts @@ -15,4 +15,5 @@ */ export { StaticAssetsStore } from './StaticAssetsStore'; -export type { StaticAsset, StaticAssetInput } from './StaticAssetsStore'; +export type { StaticAsset, StaticAssetInput } from './types'; +export { findStaticAssets } from './findStaticAssets'; diff --git a/plugins/app-backend/src/lib/assets/types.ts b/plugins/app-backend/src/lib/assets/types.ts new file mode 100644 index 0000000000..c5a01fbaea --- /dev/null +++ b/plugins/app-backend/src/lib/assets/types.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface StaticAssetInput { + path: string; + content(): Promise; +} + +export interface StaticAsset { + path: string; + content: Buffer; + lastModifiedAt: Date; +} From 3a7911ece86fbeecc0818769dc7b5820bf94b5ed Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 15:55:20 +0100 Subject: [PATCH 04/10] app-backend: added test for asset lookup Signed-off-by: Patrik Oldsberg --- plugins/app-backend/package.json | 1 + .../src/lib/assets/findStaticAssets.test.ts | 78 +++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 plugins/app-backend/src/lib/assets/findStaticAssets.test.ts diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index 4e682851bf..a7fb51f772 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -51,6 +51,7 @@ "@backstage/cli": "^0.11.0", "@backstage/types": "^0.1.1", "@types/supertest": "^2.0.8", + "mock-fs": "^5.1.0", "msw": "^0.35.0", "supertest": "^6.1.3" }, diff --git a/plugins/app-backend/src/lib/assets/findStaticAssets.test.ts b/plugins/app-backend/src/lib/assets/findStaticAssets.test.ts new file mode 100644 index 0000000000..6527c28ea4 --- /dev/null +++ b/plugins/app-backend/src/lib/assets/findStaticAssets.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import mockFs from 'mock-fs'; +import { findStaticAssets } from './findStaticAssets'; + +describe('findStaticAssets', () => { + afterEach(() => { + mockFs.restore(); + }); + + it('should find assets', async () => { + mockFs({ + '/test': { + 'a.js': 'alert("hello")', + 'a.js.map': '', + 'b.js': 'b', + 'b.js.map': '', + js: { + 'd.js': 'd', + 'd.js.map': '', + x: { + 'e.map': '', + y: { + 'e.map': '', + z: { + 'e.js': 'e', + 'e.map': '', + }, + }, + }, + }, + styles: { 'c.css': 'body { color: red; }' }, + }, + }); + + const assets = await findStaticAssets('/test'); + expect(assets.length).toBe(5); + expect(assets.map(a => a.path)).toEqual( + expect.arrayContaining([ + 'a.js', + 'b.js', + 'styles/c.css', + 'js/d.js', + 'js/x/y/z/e.js', + ]), + ); + + await expect( + assets.find(a => a.path === 'a.js')!.content(), + ).resolves.toEqual(Buffer.from('alert("hello")')); + await expect( + assets.find(a => a.path === 'b.js')!.content(), + ).resolves.toEqual(Buffer.from('b')); + await expect( + assets.find(a => a.path === 'styles/c.css')!.content(), + ).resolves.toEqual(Buffer.from('body { color: red; }')); + await expect( + assets.find(a => a.path === 'js/d.js')!.content(), + ).resolves.toEqual(Buffer.from('d')); + await expect( + assets.find(a => a.path === 'js/x/y/z/e.js')!.content(), + ).resolves.toEqual(Buffer.from('e')); + }); +}); From 7261b769358c762ba26368ff94f31ab5d1d84fb8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 16:52:59 +0100 Subject: [PATCH 05/10] app-backend: added static assets middleware + header constants Signed-off-by: Patrik Oldsberg --- .../createStaticAssetsStoreMiddleware.test.ts | 109 ++++++++++++++++++ .../createStaticAssetsStoreMiddleware.ts | 58 ++++++++++ plugins/app-backend/src/lib/assets/index.ts | 1 + plugins/app-backend/src/lib/headers.ts | 18 +++ 4 files changed, 186 insertions(+) create mode 100644 plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.test.ts create mode 100644 plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.ts create mode 100644 plugins/app-backend/src/lib/headers.ts diff --git a/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.test.ts b/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.test.ts new file mode 100644 index 0000000000..9a936a76cb --- /dev/null +++ b/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.test.ts @@ -0,0 +1,109 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import request from 'supertest'; +import { createStaticAssetsStoreMiddleware } from './createStaticAssetsStoreMiddleware'; +import { StaticAssetsStore } from './StaticAssetsStore'; + +const mockStore = { + getAsset: jest.fn(), +} as unknown as jest.Mocked; + +describe('createStaticAssetsStoreMiddleware', () => { + const app = express(); + app.use(createStaticAssetsStoreMiddleware(mockStore)); + app.use((_req, res) => { + res.status(404).end('Not Found'); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should respond with an asset', async () => { + const now = new Date(); + mockStore.getAsset.mockResolvedValueOnce({ + path: 'foo.js', + lastModifiedAt: now, + content: Buffer.from('foo'), + }); + + const res = await request(app).get('/foo.js'); + + expect(res.status).toBe(200); + expect(res.text).toBe('foo'); + expect(res.get('Content-Type')).toBe( + 'application/javascript; charset=utf-8', + ); + expect(res.get('Content-Length')).toBe('3'); + expect(res.get('Cache-Control')).toBe('public, max-age=1209600'); + expect(res.get('Last-Modified')).toBe(now.toUTCString()); + + expect(mockStore.getAsset).toHaveBeenCalledTimes(1); + expect(mockStore.getAsset).toHaveBeenCalledWith('foo.js'); + }); + + mockStore.getAsset.mockResolvedValueOnce(undefined); + + it('should respond with not found', async () => { + const res = await request(app).get('/foo.js'); + + expect(res.status).toBe(404); + expect(res.text).toBe('Not Found'); + + expect(mockStore.getAsset).toHaveBeenCalledTimes(1); + expect(mockStore.getAsset).toHaveBeenCalledWith('foo.js'); + }); + + it('should handle other content type', async () => { + mockStore.getAsset.mockResolvedValueOnce({ + path: 'foo.css', + lastModifiedAt: new Date(), + content: Buffer.from('foo'), + }); + + const res = await request(app).get('/foo.css'); + + expect(res.status).toBe(200); + expect(res.text).toBe('foo'); + expect(res.get('Content-Type')).toBe('text/css; charset=utf-8'); + expect(res.get('Content-Length')).toBe('3'); + + expect(mockStore.getAsset).toHaveBeenCalledTimes(1); + expect(mockStore.getAsset).toHaveBeenLastCalledWith('foo.css'); + }); + + it('should handle unknown content types', async () => { + mockStore.getAsset.mockResolvedValueOnce({ + path: 'foo.notavalidextension', + lastModifiedAt: new Date(), + content: Buffer.from('foo'), + }); + + const res = await request(app).get('/foo.notavalidextension'); + + expect(res.status).toBe(200); + expect(res.body).toEqual(Buffer.from('foo')); + expect(res.get('Content-Type')).toBe('application/octet-stream'); + expect(res.get('Content-Length')).toBe('3'); + + expect(mockStore.getAsset).toHaveBeenCalledTimes(1); + expect(mockStore.getAsset).toHaveBeenLastCalledWith( + 'foo.notavalidextension', + ); + }); +}); diff --git a/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.ts b/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.ts new file mode 100644 index 0000000000..55fe30cbc3 --- /dev/null +++ b/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.ts @@ -0,0 +1,58 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { extname } from 'path'; +import { RequestHandler } from 'express'; +import { StaticAssetsStore } from './StaticAssetsStore'; +import { CACHE_CONTROL_MAX_CACHE } from '../headers'; + +export function createStaticAssetsStoreMiddleware( + store: StaticAssetsStore, +): RequestHandler { + return (req, res, next) => { + if (req.method !== 'GET' && req.method !== 'HEAD') { + next(); + return; + } + + Promise.resolve( + (async () => { + // Drop leading slashes from the incoming path + const path = req.path.startsWith('/') ? req.path.slice(1) : req.path; + + const asset = await store.getAsset(path); + if (!asset) { + next(); + return; + } + + // Set the Content-Type header, falling back to octet-stream + const ext = extname(asset.path); + if (ext) { + res.type(ext); + } else { + res.type('bin'); + } + + // Same as our express.static override + res.setHeader('Cache-Control', CACHE_CONTROL_MAX_CACHE); + res.setHeader('Last-Modified', asset.lastModifiedAt.toUTCString()); + + res.send(asset.content); + })(), + ).catch(next); + }; +} diff --git a/plugins/app-backend/src/lib/assets/index.ts b/plugins/app-backend/src/lib/assets/index.ts index c8cb838dd6..cb83f153dd 100644 --- a/plugins/app-backend/src/lib/assets/index.ts +++ b/plugins/app-backend/src/lib/assets/index.ts @@ -17,3 +17,4 @@ export { StaticAssetsStore } from './StaticAssetsStore'; export type { StaticAsset, StaticAssetInput } from './types'; export { findStaticAssets } from './findStaticAssets'; +export { createStaticAssetsStoreMiddleware } from './createStaticAssetsStoreMiddleware'; diff --git a/plugins/app-backend/src/lib/headers.ts b/plugins/app-backend/src/lib/headers.ts new file mode 100644 index 0000000000..1bee2bbb37 --- /dev/null +++ b/plugins/app-backend/src/lib/headers.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const CACHE_CONTROL_NO_CACHE = 'no-store, max-age=0'; +export const CACHE_CONTROL_MAX_CACHE = 'public, max-age=1209600'; // 14 days From 729b188045846e3107afccf133d630c2febaecf2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 16:58:01 +0100 Subject: [PATCH 06/10] app-backend: update router to use static assets store and new cache headers Signed-off-by: Patrik Oldsberg --- .../app-backend/src/service/router.test.ts | 2 +- plugins/app-backend/src/service/router.ts | 48 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/plugins/app-backend/src/service/router.test.ts b/plugins/app-backend/src/service/router.test.ts index bbbf115834..30fa5454c0 100644 --- a/plugins/app-backend/src/service/router.test.ts +++ b/plugins/app-backend/src/service/router.test.ts @@ -81,7 +81,7 @@ describe('createRouter', () => { 'returns %s with default Cache-Control header', async file => { const response = await request(app).get(file); - expect(response.header['cache-control']).toBe('public, max-age=0'); + expect(response.header['cache-control']).toBe('public, max-age=1209600'); }, ); }); diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index 171684a964..cc476eab91 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { notFoundHandler, resolvePackagePath } from '@backstage/backend-common'; +import { + notFoundHandler, + PluginDatabaseManager, + resolvePackagePath, +} from '@backstage/backend-common'; import { Config } from '@backstage/config'; import helmet from 'helmet'; import express from 'express'; @@ -23,6 +27,15 @@ import fs from 'fs-extra'; import { resolve as resolvePath } from 'path'; import { Logger } from 'winston'; import { injectConfig, readConfigs } from '../lib/config'; +import { + StaticAssetsStore, + findStaticAssets, + createStaticAssetsStoreMiddleware, +} from '../lib/assets'; +import { + CACHE_CONTROL_MAX_CACHE, + CACHE_CONTROL_NO_CACHE, +} from '../lib/headers'; // express uses mime v1 while we only have types for mime v2 type Mime = { lookup(arg0: string): string }; @@ -31,6 +44,12 @@ export interface RouterOptions { config: Config; logger: Logger; + /** + * The plugin database manager which of provided, the app-backend will use to cache previously + * deployed static assets. + */ + database?: PluginDatabaseManager; + /** * The name of the app package that content should be served from. The same app package should be * added as a dependency to the backend package in order for it to be accessible at runtime. @@ -94,7 +113,28 @@ export async function createRouter( // Use a separate router for static content so that a fallback can be provided by backend const staticRouter = Router(); - staticRouter.use(express.static(resolvePath(appDistDir, 'static'))); + staticRouter.use( + express.static(resolvePath(appDistDir, 'static'), { + setHeaders: res => { + res.setHeader('Cache-Control', CACHE_CONTROL_MAX_CACHE); + }, + }), + ); + + if (options.database) { + const store = await StaticAssetsStore.create({ + logger, + database: await options.database.getClient(), + }); + + const assets = await findStaticAssets(staticDir); + await store.storeAssets(assets); + // Remove any assets that are older than 7 days + await store.trimAssets({ maxAgeSeconds: 60 * 60 * 24 * 7 }); + + staticRouter.use(createStaticAssetsStoreMiddleware(store)); + } + if (staticFallbackHandler) { staticRouter.use(staticFallbackHandler); } @@ -109,7 +149,7 @@ export async function createRouter( if ( (express.static.mime as unknown as Mime).lookup(path) === 'text/html' ) { - res.setHeader('Cache-Control', 'no-store, max-age=0'); + res.setHeader('Cache-Control', CACHE_CONTROL_NO_CACHE); } }, }), @@ -119,7 +159,7 @@ export async function createRouter( headers: { // The Cache-Control header instructs the browser to not cache the index.html since it might // link to static assets from recently deployed versions. - 'cache-control': 'no-store, max-age=0', + 'cache-control': CACHE_CONTROL_NO_CACHE, }, }); }); From eb00e8af146a7b8ecf950c76b3330bf68d467910 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 17:02:26 +0100 Subject: [PATCH 07/10] app-backend: changeset and API report for asset cache and header updates Signed-off-by: Patrik Oldsberg --- .changeset/wild-pugs-call.md | 5 +++++ .changeset/witty-avocados-bow.md | 7 +++++++ plugins/app-backend/api-report.md | 2 ++ 3 files changed, 14 insertions(+) create mode 100644 .changeset/wild-pugs-call.md create mode 100644 .changeset/witty-avocados-bow.md diff --git a/.changeset/wild-pugs-call.md b/.changeset/wild-pugs-call.md new file mode 100644 index 0000000000..6bcc951ac7 --- /dev/null +++ b/.changeset/wild-pugs-call.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Updated the cache control headers for static assets to instruct clients to cache them for 14 days. diff --git a/.changeset/witty-avocados-bow.md b/.changeset/witty-avocados-bow.md new file mode 100644 index 0000000000..406088a7dd --- /dev/null +++ b/.changeset/witty-avocados-bow.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-app-backend': patch +--- + +Added a new asset cache that stores static assets from previous deployments in the database. This fixes an issue where users have old browser tabs open and try to lazy-load static assets that no longer exist in the latest version. + +The asset cache is enabled by passing the `database` option to `createRouter`. diff --git a/plugins/app-backend/api-report.md b/plugins/app-backend/api-report.md index d742b10442..50d1b7bc34 100644 --- a/plugins/app-backend/api-report.md +++ b/plugins/app-backend/api-report.md @@ -6,6 +6,7 @@ import { Config } from '@backstage/config'; import express from 'express'; import { Logger as Logger_2 } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -19,6 +20,7 @@ export interface RouterOptions { appPackageName: string; // (undocumented) config: Config; + database?: PluginDatabaseManager; disableConfigInjection?: boolean; // (undocumented) logger: Logger_2; From fb08e2f285a0f5fa1bbe6f8d020f0b5ca0b3aaea Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 17:14:03 +0100 Subject: [PATCH 08/10] backend,create-app: update app-backend setup to forward database Signed-off-by: Patrik Oldsberg --- .changeset/sixty-laws-laugh.md | 22 +++++++++++++++++++ packages/backend/src/plugins/app.ts | 2 ++ .../packages/backend/src/plugins/app.ts | 2 ++ 3 files changed, 26 insertions(+) create mode 100644 .changeset/sixty-laws-laugh.md diff --git a/.changeset/sixty-laws-laugh.md b/.changeset/sixty-laws-laugh.md new file mode 100644 index 0000000000..874859da4e --- /dev/null +++ b/.changeset/sixty-laws-laugh.md @@ -0,0 +1,22 @@ +--- +'@backstage/create-app': patch +--- + +Updated the configuration of the `app-backend` plugin to enable the static asset store by passing on `database` from the plugin environment to `createRouter`. + +To apply this change to an existing app, make the following change to `packages/backend/src/plugins/app.ts`: + +```diff + export default async function createPlugin({ + logger, + config, ++ database, + }: PluginEnvironment): Promise { + return await createRouter({ + logger, + config, ++ database, + appPackageName: 'app', + }); + } +``` diff --git a/packages/backend/src/plugins/app.ts b/packages/backend/src/plugins/app.ts index b5f05f2255..0747d890e1 100644 --- a/packages/backend/src/plugins/app.ts +++ b/packages/backend/src/plugins/app.ts @@ -21,10 +21,12 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, config, + database, }: PluginEnvironment): Promise { return await createRouter({ logger, config, + database, appPackageName: 'example-app', }); } diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/app.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/app.ts index 07fb04fca2..14e19a19b1 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/app.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/app.ts @@ -5,10 +5,12 @@ import { PluginEnvironment } from '../types'; export default async function createPlugin({ logger, config, + database, }: PluginEnvironment): Promise { return await createRouter({ logger, config, + database, appPackageName: 'app', }); } From ccd9bcb86ee254390fb307bcfe1b598b07daede9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 17:37:53 +0100 Subject: [PATCH 09/10] app-backend: some refactoring, renaming, and docs Signed-off-by: Patrik Oldsberg --- .../app-backend/src/lib/assets/StaticAssetsStore.ts | 4 ++-- ...e.test.ts => createStaticAssetMiddleware.test.ts} | 6 +++--- ...eMiddleware.ts => createStaticAssetMiddleware.ts} | 12 +++++++++--- .../app-backend/src/lib/assets/findStaticAssets.ts | 5 +++++ plugins/app-backend/src/lib/assets/index.ts | 8 ++++++-- plugins/app-backend/src/lib/assets/types.ts | 4 ++++ plugins/app-backend/src/service/router.ts | 4 ++-- 7 files changed, 31 insertions(+), 12 deletions(-) rename plugins/app-backend/src/lib/assets/{createStaticAssetsStoreMiddleware.test.ts => createStaticAssetMiddleware.test.ts} (94%) rename plugins/app-backend/src/lib/assets/{createStaticAssetsStoreMiddleware.ts => createStaticAssetMiddleware.ts} (85%) diff --git a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts index 35a5171c83..6b393ce036 100644 --- a/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts +++ b/plugins/app-backend/src/lib/assets/StaticAssetsStore.ts @@ -19,7 +19,7 @@ import { Knex } from 'knex'; import { Logger } from 'winston'; import { DateTime } from 'luxon'; import partition from 'lodash/partition'; -import { StaticAsset, StaticAssetInput } from './types'; +import { StaticAsset, StaticAssetInput, StaticAssetProvider } from './types'; const migrationsDir = resolvePackagePath( '@backstage/plugin-app-backend', @@ -43,7 +43,7 @@ export interface StaticAssetsStoreOptions { * * @internal */ -export class StaticAssetsStore { +export class StaticAssetsStore implements StaticAssetProvider { #db: Knex; #logger: Logger; diff --git a/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.test.ts b/plugins/app-backend/src/lib/assets/createStaticAssetMiddleware.test.ts similarity index 94% rename from plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.test.ts rename to plugins/app-backend/src/lib/assets/createStaticAssetMiddleware.test.ts index 9a936a76cb..63cb3a121f 100644 --- a/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.test.ts +++ b/plugins/app-backend/src/lib/assets/createStaticAssetMiddleware.test.ts @@ -16,16 +16,16 @@ import express from 'express'; import request from 'supertest'; -import { createStaticAssetsStoreMiddleware } from './createStaticAssetsStoreMiddleware'; +import { createStaticAssetMiddleware } from './createStaticAssetMiddleware'; import { StaticAssetsStore } from './StaticAssetsStore'; const mockStore = { getAsset: jest.fn(), } as unknown as jest.Mocked; -describe('createStaticAssetsStoreMiddleware', () => { +describe('createStaticAssetMiddleware', () => { const app = express(); - app.use(createStaticAssetsStoreMiddleware(mockStore)); + app.use(createStaticAssetMiddleware(mockStore)); app.use((_req, res) => { res.status(404).end('Not Found'); }); diff --git a/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.ts b/plugins/app-backend/src/lib/assets/createStaticAssetMiddleware.ts similarity index 85% rename from plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.ts rename to plugins/app-backend/src/lib/assets/createStaticAssetMiddleware.ts index 55fe30cbc3..60458436dd 100644 --- a/plugins/app-backend/src/lib/assets/createStaticAssetsStoreMiddleware.ts +++ b/plugins/app-backend/src/lib/assets/createStaticAssetMiddleware.ts @@ -16,11 +16,16 @@ import { extname } from 'path'; import { RequestHandler } from 'express'; -import { StaticAssetsStore } from './StaticAssetsStore'; +import { StaticAssetProvider } from './types'; import { CACHE_CONTROL_MAX_CACHE } from '../headers'; -export function createStaticAssetsStoreMiddleware( - store: StaticAssetsStore, +/** + * Creates a middleware that serves static assets from a static asset provider + * + * @internal + */ +export function createStaticAssetMiddleware( + store: StaticAssetProvider, ): RequestHandler { return (req, res, next) => { if (req.method !== 'GET' && req.method !== 'HEAD') { @@ -28,6 +33,7 @@ export function createStaticAssetsStoreMiddleware( return; } + // Let's not assume we're in promise-router Promise.resolve( (async () => { // Drop leading slashes from the incoming path diff --git a/plugins/app-backend/src/lib/assets/findStaticAssets.ts b/plugins/app-backend/src/lib/assets/findStaticAssets.ts index 1e4ba83982..7ecea7146e 100644 --- a/plugins/app-backend/src/lib/assets/findStaticAssets.ts +++ b/plugins/app-backend/src/lib/assets/findStaticAssets.ts @@ -19,6 +19,11 @@ import globby from 'globby'; import { StaticAssetInput } from './types'; import { resolveSafeChildPath } from '@backstage/backend-common'; +/** + * Finds all static assets within a directory + * + * @internal + */ export async function findStaticAssets( staticDir: string, ): Promise { diff --git a/plugins/app-backend/src/lib/assets/index.ts b/plugins/app-backend/src/lib/assets/index.ts index cb83f153dd..3a158b4392 100644 --- a/plugins/app-backend/src/lib/assets/index.ts +++ b/plugins/app-backend/src/lib/assets/index.ts @@ -15,6 +15,10 @@ */ export { StaticAssetsStore } from './StaticAssetsStore'; -export type { StaticAsset, StaticAssetInput } from './types'; +export type { + StaticAsset, + StaticAssetInput, + StaticAssetProvider, +} from './types'; export { findStaticAssets } from './findStaticAssets'; -export { createStaticAssetsStoreMiddleware } from './createStaticAssetsStoreMiddleware'; +export { createStaticAssetMiddleware } from './createStaticAssetMiddleware'; diff --git a/plugins/app-backend/src/lib/assets/types.ts b/plugins/app-backend/src/lib/assets/types.ts index c5a01fbaea..464d818d3e 100644 --- a/plugins/app-backend/src/lib/assets/types.ts +++ b/plugins/app-backend/src/lib/assets/types.ts @@ -24,3 +24,7 @@ export interface StaticAsset { content: Buffer; lastModifiedAt: Date; } + +export interface StaticAssetProvider { + getAsset(path: string): Promise; +} diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index cc476eab91..af13e60d3e 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -30,7 +30,7 @@ import { injectConfig, readConfigs } from '../lib/config'; import { StaticAssetsStore, findStaticAssets, - createStaticAssetsStoreMiddleware, + createStaticAssetMiddleware, } from '../lib/assets'; import { CACHE_CONTROL_MAX_CACHE, @@ -132,7 +132,7 @@ export async function createRouter( // Remove any assets that are older than 7 days await store.trimAssets({ maxAgeSeconds: 60 * 60 * 24 * 7 }); - staticRouter.use(createStaticAssetsStoreMiddleware(store)); + staticRouter.use(createStaticAssetMiddleware(store)); } if (staticFallbackHandler) { From 8aebc7b0db404d40f498d87efbcbc1210854adc9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 29 Dec 2021 18:04:37 +0100 Subject: [PATCH 10/10] app-backend: clarify relation between database and staticFallbackHandler Signed-off-by: Patrik Oldsberg --- plugins/app-backend/src/service/router.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/app-backend/src/service/router.ts b/plugins/app-backend/src/service/router.ts index af13e60d3e..b666f323db 100644 --- a/plugins/app-backend/src/service/router.ts +++ b/plugins/app-backend/src/service/router.ts @@ -45,8 +45,9 @@ export interface RouterOptions { logger: Logger; /** - * The plugin database manager which of provided, the app-backend will use to cache previously - * deployed static assets. + * If a database is provided it will be used to cache previously deployed static assets. + * + * This is a built-in alternative to using a `staticFallbackHandler`. */ database?: PluginDatabaseManager; @@ -64,6 +65,11 @@ export interface RouterOptions { * This can be used to avoid issues with clients on older deployment versions trying to access lazy * loaded content that is no longer present. Typically the requests would fall back to a long-term * object store where all recently deployed versions of the app are present. + * + * Another option is to provide a `database` that will take care of storing the static assets instead. + * + * If both `database` and `staticFallbackHandler` are provided, the `database` will attempt to serve + * static assets first, and if they are not found, the `staticFallbackHandler` will be called. */ staticFallbackHandler?: express.Handler;