[TechDocs] Added configuration for local publishing target

This patch adds a configuration option for setting the "local"
techdocs target directory. The target directory can be set using
the "techdocs.publisher.local.publishDirectory".

This fixes two "TODOs" in the "plugins/techdocs-node/src/stages
/publish/local.ts" file:

*  Use a more persistent storage than node_modules or /tmp directory.
   Make it configurable with techdocs.publisher.local.publishDirectory
*  Move the logic of setting staticDocsDir based on config over to
   fromConfig, and set the value as a class parameter.

Signed-off-by: Niklas Aronsson <niklasar@axis.com>
This commit is contained in:
Niklas Aronsson
2022-04-25 13:15:21 +02:00
parent fe391c8bb4
commit 860765ff45
6 changed files with 82 additions and 25 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-node': minor
---
Added local publishing target directory `config`: `techdocs.publisher.local.publishDirectory`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs-backend': minor
---
Added local publishing target directory `config`: `techdocs.publisher.local.publishDirectory`
+6
View File
@@ -74,6 +74,12 @@ techdocs:
type: 'local'
# Optional when techdocs.publisher.type is set to 'local'.
local:
# (Optional). Set this to specify where the generated documentation is stored.
publishDirectory: '/path/to/local/directory'
# Required when techdocs.publisher.type is set to 'googleGcs'. Skip otherwise.
googleGcs:
+7
View File
@@ -66,6 +66,13 @@ export interface Config {
publisher?:
| {
type: 'local';
local?: {
/**
* Directory to store generated static files.
*/
publishDirectory?: string;
};
}
| {
type: 'awsS3';
@@ -236,5 +236,32 @@ describe('local publisher', () => {
);
expect(response.status).toBe(404);
});
it('should work with a configured directory', async () => {
const customConfig = new ConfigReader({
techdocs: {
publisher: {
local: {
publishDirectory: tmpDir,
},
},
},
});
mockFs({
[tmpDir]: {
'index.html': 'found it',
},
});
const legacyPublisher = LocalPublish.fromConfig(
customConfig,
logger,
testDiscovery,
);
app = express().use(legacyPublisher.docsRouter());
const response = await request(app).get('/index.html');
expect(response.status).toBe(200);
expect(response.text).toEqual('found it');
});
});
});
@@ -44,40 +44,27 @@ import {
} from './helpers';
import { ForwardedError } from '@backstage/errors';
// TODO: Use a more persistent storage than node_modules or /tmp directory.
// Make it configurable with techdocs.publisher.local.publishDirectory
let staticDocsDir = '';
try {
staticDocsDir = resolvePackagePath(
'@backstage/plugin-techdocs-backend',
'static/docs',
);
} catch (err) {
// This will most probably never be used.
// The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node
// on CI/CD without installing techdocs backend plugin.
staticDocsDir = os.tmpdir();
}
/**
* Local publisher which uses the local filesystem to store the generated static files. It uses a directory
* called "static" at the root of techdocs-backend plugin.
* Local publisher which uses the local filesystem to store the generated static files. It uses by default a
* directory called "static" at the root of techdocs-backend plugin unless a directory has been configured by
* "techdocs.publisher.local.publishDirectory".
*/
export class LocalPublish implements PublisherBase {
private readonly legacyPathCasing: boolean;
private readonly logger: Logger;
private readonly discovery: PluginEndpointDiscovery;
private readonly staticDocsDir: string;
// TODO: Move the logic of setting staticDocsDir based on config over to
// fromConfig, and set the value as a class parameter.
constructor(options: {
logger: Logger;
discovery: PluginEndpointDiscovery;
legacyPathCasing: boolean;
staticDocsDir: string;
}) {
this.logger = options.logger;
this.discovery = options.discovery;
this.legacyPathCasing = options.legacyPathCasing;
this.staticDocsDir = options.staticDocsDir;
}
static fromConfig(
@@ -90,10 +77,28 @@ export class LocalPublish implements PublisherBase {
'techdocs.legacyUseCaseSensitiveTripletPaths',
) || false;
let staticDocsDir = config.getOptionalString(
'techdocs.publisher.local.publishDirectory',
);
if (!staticDocsDir) {
try {
staticDocsDir = resolvePackagePath(
'@backstage/plugin-techdocs-backend',
'static/docs',
);
} catch (err) {
// This will most probably never be used.
// The try/catch is introduced so that techdocs-cli can import @backstage/plugin-techdocs-node
// on CI/CD without installing techdocs backend plugin.
staticDocsDir = os.tmpdir();
}
}
return new LocalPublish({
logger,
discovery,
legacyPathCasing,
staticDocsDir,
});
}
@@ -144,7 +149,7 @@ export class LocalPublish implements PublisherBase {
const techdocsApiUrl = await this.discovery.getBaseUrl('techdocs');
const publishedFilePaths = (await getFileTreeRecursively(publishDir)).map(
abs => {
return abs.split(`${staticDocsDir}/`)[1];
return abs.split(`${this.staticDocsDir}/`)[1];
},
);
@@ -222,9 +227,8 @@ export class LocalPublish implements PublisherBase {
// Otherwise, redirect to the new path.
return res.redirect(301, req.baseUrl + newPath);
});
router.use(
express.static(staticDocsDir, {
express.static(this.staticDocsDir, {
// Handle content-type header the same as all other publishers.
setHeaders: (res, filePath) => {
const fileExtension = path.extname(filePath);
@@ -275,13 +279,16 @@ export class LocalPublish implements PublisherBase {
concurrency = 25,
}): Promise<void> {
// Iterate through every file in the root of the publisher.
const files = await getFileTreeRecursively(staticDocsDir);
const files = await getFileTreeRecursively(this.staticDocsDir);
const limit = createLimiter(concurrency);
await Promise.all(
files.map(f =>
limit(async file => {
const relativeFile = file.replace(`${staticDocsDir}${path.sep}`, '');
const relativeFile = file.replace(
`${this.staticDocsDir}${path.sep}`,
'',
);
const newFile = lowerCaseEntityTripletInStoragePath(relativeFile);
// If all parts are already lowercase, ignore.
@@ -311,7 +318,7 @@ export class LocalPublish implements PublisherBase {
* Utility wrapper around path.join(), used to control legacy case logic.
*/
protected staticEntityPathJoin(...allParts: string[]): string {
let staticEntityPath = staticDocsDir;
let staticEntityPath = this.staticDocsDir;
allParts
.map(part => part.split(path.sep))