@backstage/plugin-techdocs-node 2.0.3-next.2 → 2.2.0-next.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"azureBlobStorage.cjs.js","sources":["../../../src/stages/publish/azureBlobStorage.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { DefaultAzureCredential } from '@azure/identity';\nimport {\n BlobServiceClient,\n ContainerClient,\n StorageSharedKeyCredential,\n} from '@azure/storage-blob';\nimport { Entity, CompoundEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { ForwardedError, toError } from '@backstage/errors';\nimport express from 'express';\nimport JSON5 from 'json5';\nimport limiterFactory from 'p-limit';\nimport { default as path, default as platformPath } from 'node:path';\nimport {\n bulkStorageOperation,\n getCloudPathForLocalPath,\n getFileTreeRecursively,\n getHeadersForFileExtension,\n lowerCaseEntityTriplet,\n getStaleFiles,\n lowerCaseEntityTripletInStoragePath,\n} from './helpers';\nimport {\n PublisherBase,\n PublishRequest,\n PublishResponse,\n ReadinessResponse,\n TechDocsMetadata,\n} from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\n// The number of batches that may be ongoing at the same time.\nconst BATCH_CONCURRENCY = 3;\n\nexport class AzureBlobStoragePublish implements PublisherBase {\n private readonly storageClient: BlobServiceClient;\n private readonly containerName: string;\n private readonly legacyPathCasing: boolean;\n private readonly logger: LoggerService;\n\n constructor(options: {\n storageClient: BlobServiceClient;\n containerName: string;\n legacyPathCasing: boolean;\n logger: LoggerService;\n }) {\n this.storageClient = options.storageClient;\n this.containerName = options.containerName;\n this.legacyPathCasing = options.legacyPathCasing;\n this.logger = options.logger;\n }\n\n static fromConfig(config: Config, logger: LoggerService): PublisherBase {\n let storageClient: BlobServiceClient;\n let containerName = '';\n try {\n containerName = config.getString(\n 'techdocs.publisher.azureBlobStorage.containerName',\n );\n } catch (error) {\n throw new Error(\n \"Since techdocs.publisher.type is set to 'azureBlobStorage' in your app config, \" +\n 'techdocs.publisher.azureBlobStorage.containerName is required.',\n );\n }\n\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n\n // Give more priority for connectionString, if configured, return the AzureBlobStoragePublish object here itself\n const connectionStringKey =\n 'techdocs.publisher.azureBlobStorage.connectionString';\n const connectionString = config.getOptionalString(connectionStringKey);\n\n if (connectionString) {\n logger.info(\n `Using '${connectionStringKey}' configuration to create storage client`,\n );\n storageClient = BlobServiceClient.fromConnectionString(connectionString);\n } else {\n let accountName = '';\n try {\n accountName = config.getString(\n 'techdocs.publisher.azureBlobStorage.credentials.accountName',\n );\n } catch (error) {\n throw new Error(\n \"Since techdocs.publisher.type is set to 'azureBlobStorage' in your app config, \" +\n 'techdocs.publisher.azureBlobStorage.credentials.accountName is required.',\n );\n }\n\n // Credentials is an optional config. If missing, default Azure Blob Storage environment variables will be used.\n // https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app\n const accountKey = config.getOptionalString(\n 'techdocs.publisher.azureBlobStorage.credentials.accountKey',\n );\n\n let credential;\n if (accountKey) {\n credential = new StorageSharedKeyCredential(accountName, accountKey);\n } else {\n credential = new DefaultAzureCredential();\n }\n\n storageClient = new BlobServiceClient(\n `https://${accountName}.blob.core.windows.net`,\n credential,\n );\n }\n\n return new AzureBlobStoragePublish({\n storageClient: storageClient,\n containerName: containerName,\n legacyPathCasing: legacyPathCasing,\n logger: logger,\n });\n }\n\n async getReadiness(): Promise<ReadinessResponse> {\n try {\n const response = await this.storageClient\n .getContainerClient(this.containerName)\n .getProperties();\n\n if (response._response.status === 200) {\n return {\n isAvailable: true,\n };\n }\n\n if (response._response.status >= 400) {\n this.logger.error(\n `Failed to retrieve metadata from ${response._response.request.url} with status code ${response._response.status}.`,\n );\n }\n } catch (e) {\n this.logger.error(\n `from Azure Blob Storage client library: ${toError(e).message}`,\n );\n }\n\n this.logger.error(\n `Could not retrieve metadata about the Azure Blob Storage container ${this.containerName}. ` +\n 'Make sure that the Azure project and container exist and the access key is setup correctly ' +\n 'techdocs.publisher.azureBlobStorage.credentials defined in app config has correct permissions. ' +\n 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',\n );\n\n return { isAvailable: false };\n }\n\n /**\n * Upload all the files from the generated `directory` to the Azure Blob Storage container.\n * Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html\n */\n async publish({\n entity,\n directory,\n }: PublishRequest): Promise<PublishResponse> {\n const objects: string[] = [];\n const useLegacyPathCasing = this.legacyPathCasing;\n\n // First, try to retrieve a list of all individual files currently existing\n const remoteFolder = getCloudPathForLocalPath(\n entity,\n undefined,\n useLegacyPathCasing,\n );\n let existingFiles: string[] = [];\n try {\n existingFiles = await this.getAllBlobsFromContainer({\n prefix: remoteFolder,\n maxPageSize: BATCH_CONCURRENCY,\n });\n } catch (e) {\n this.logger.error(\n `Unable to list files for Entity ${entity.metadata.name}: ${\n toError(e).message\n }`,\n );\n }\n\n // Then, merge new files into the same folder\n let absoluteFilesToUpload;\n let container: ContainerClient;\n try {\n // Remove the absolute path prefix of the source directory\n // Path of all files to upload, relative to the root of the source directory\n // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png']\n absoluteFilesToUpload = await getFileTreeRecursively(directory);\n\n container = this.storageClient.getContainerClient(this.containerName);\n const failedOperations: Error[] = [];\n await bulkStorageOperation(\n async absoluteFilePath => {\n const relativeFilePath = path.normalize(\n path.relative(directory, absoluteFilePath),\n );\n const remotePath = getCloudPathForLocalPath(\n entity,\n relativeFilePath,\n useLegacyPathCasing,\n );\n objects.push(remotePath);\n const response = await container\n .getBlockBlobClient(remotePath)\n .uploadFile(absoluteFilePath);\n\n if (response._response.status >= 400) {\n failedOperations.push(\n new Error(\n `Upload failed for ${absoluteFilePath} with status code ${response._response.status}`,\n ),\n );\n }\n\n return response;\n },\n absoluteFilesToUpload,\n { concurrencyLimit: BATCH_CONCURRENCY },\n );\n\n if (failedOperations.length > 0) {\n throw new Error(\n failedOperations\n .map(r => r.message)\n .filter(Boolean)\n .join(' '),\n );\n }\n\n this.logger.info(\n `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`,\n );\n } catch (e) {\n const errorMessage = `Unable to upload file(s) to Azure. ${e}`;\n this.logger.error(errorMessage);\n throw new Error(errorMessage);\n }\n\n // Last, try to remove the files that were *only* present previously\n try {\n const relativeFilesToUpload = absoluteFilesToUpload.map(\n absoluteFilePath =>\n getCloudPathForLocalPath(\n entity,\n path.relative(directory, absoluteFilePath),\n useLegacyPathCasing,\n ),\n );\n\n const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles);\n\n await bulkStorageOperation(\n async relativeFilePath => {\n return await container.deleteBlob(relativeFilePath);\n },\n staleFiles,\n { concurrencyLimit: BATCH_CONCURRENCY },\n );\n\n this.logger.info(\n `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`,\n );\n } catch (error) {\n const errorMessage = `Unable to delete file(s) from Azure. ${error}`;\n this.logger.error(errorMessage);\n }\n\n return { objects };\n }\n\n async fetchTechDocsMetadata(\n entityName: CompoundEntityRef,\n ): Promise<TechDocsMetadata> {\n const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;\n const entityRootDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n try {\n const techdocsMetadataJson = await new Promise<Buffer>(\n (resolve, reject) => {\n const fileStreamChunks: Array<any> = [];\n this.storageClient\n .getContainerClient(this.containerName)\n .getBlockBlobClient(`${entityRootDir}/techdocs_metadata.json`)\n .download()\n .then(res => {\n const body = res.readableStreamBody;\n if (!body) {\n reject(new Error(`Unable to parse the response data`));\n return;\n }\n body\n .on('error', reject)\n .on('data', chunk => {\n fileStreamChunks.push(chunk);\n })\n .on('end', () => {\n resolve(Buffer.concat(fileStreamChunks));\n });\n })\n .catch(reject);\n },\n );\n\n if (!techdocsMetadataJson) {\n throw new Error(\n `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`,\n );\n }\n const techdocsMetadata = JSON5.parse(\n techdocsMetadataJson.toString('utf-8'),\n );\n return techdocsMetadata;\n } catch (e) {\n throw new ForwardedError('TechDocs metadata fetch failed', e);\n }\n }\n\n /**\n * Express route middleware to serve static files on a route in techdocs-backend.\n */\n docsRouter(): express.Handler {\n return (req, res) => {\n // Decode and trim the leading forward slash\n const decodedUri = decodeURI(req.path.replace(/^\\//, ''));\n\n // filePath example - /default/Component/documented-component/index.html\n const filePath = this.legacyPathCasing\n ? decodedUri\n : lowerCaseEntityTripletInStoragePath(decodedUri);\n\n // Files with different extensions (CSS, HTML) need to be served with different headers\n const fileExtension = platformPath.extname(filePath);\n const responseHeaders = getHeadersForFileExtension(fileExtension);\n\n const blobClient = this.storageClient\n .getContainerClient(this.containerName)\n .getBlockBlobClient(filePath);\n\n blobClient\n .download()\n .then(downloadRes => {\n if (!downloadRes.readableStreamBody) {\n throw new Error('Unable to parse the response data');\n }\n for (const [headerKey, headerValue] of Object.entries(\n responseHeaders,\n )) {\n res.setHeader(headerKey, headerValue);\n }\n downloadRes.readableStreamBody.pipe(res);\n })\n .catch(e => {\n this.logger.warn(\n `TechDocs Azure router failed to serve content from container ${this.containerName} at path ${filePath}: ${e.message}`,\n );\n if (!res.headersSent) {\n res.status(404).send('File Not Found');\n } else {\n res.destroy();\n }\n });\n };\n }\n\n /**\n * A helper function which checks if index.html of an Entity's docs site is available. This\n * can be used to verify if there are any pre-generated docs available to serve.\n */\n hasDocsBeenGenerated(entity: Entity): Promise<boolean> {\n const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;\n const entityRootDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n return this.storageClient\n .getContainerClient(this.containerName)\n .getBlockBlobClient(`${entityRootDir}/index.html`)\n .exists();\n }\n\n protected async renameBlob(\n originalName: string,\n newName: string,\n removeOriginal = false,\n ): Promise<void> {\n const container = this.storageClient.getContainerClient(this.containerName);\n const blob = container.getBlobClient(newName);\n const { url } = container.getBlobClient(originalName);\n const response = await blob.beginCopyFromURL(url);\n await response.pollUntilDone();\n if (removeOriginal) {\n await container.deleteBlob(originalName);\n }\n }\n\n protected async renameBlobToLowerCase(\n originalPath: string,\n removeOriginal: boolean,\n ) {\n let newPath;\n try {\n newPath = lowerCaseEntityTripletInStoragePath(originalPath);\n } catch (e) {\n this.logger.warn(toError(e).message);\n return;\n }\n\n if (originalPath === newPath) return;\n try {\n this.logger.debug(`Migrating ${originalPath}`);\n await this.renameBlob(originalPath, newPath, removeOriginal);\n } catch (e) {\n this.logger.warn(\n `Unable to migrate ${originalPath}: ${toError(e).message}`,\n );\n }\n }\n\n async migrateDocsCase({\n removeOriginal = false,\n concurrency = 25,\n }): Promise<void> {\n const promises = [];\n const limiter = limiterFactory(concurrency);\n const container = this.storageClient.getContainerClient(this.containerName);\n\n for await (const blob of container.listBlobsFlat()) {\n promises.push(\n limiter(\n this.renameBlobToLowerCase.bind(this),\n blob.name,\n removeOriginal,\n ),\n );\n }\n\n await Promise.all(promises);\n }\n\n protected async getAllBlobsFromContainer({\n prefix,\n maxPageSize,\n }: {\n prefix: string;\n maxPageSize: number;\n }): Promise<string[]> {\n const blobs: string[] = [];\n const container = this.storageClient.getContainerClient(this.containerName);\n\n let iterator = container.listBlobsFlat({ prefix }).byPage({ maxPageSize });\n let response = (await iterator.next()).value;\n\n do {\n for (const blob of response?.segment?.blobItems ?? []) {\n blobs.push(blob.name);\n }\n iterator = container\n .listBlobsFlat({ prefix })\n .byPage({ continuationToken: response.continuationToken, maxPageSize });\n response = (await iterator.next()).value;\n } while (response && response.continuationToken);\n\n return blobs;\n }\n}\n"],"names":["BlobServiceClient","StorageSharedKeyCredential","DefaultAzureCredential","toError","getCloudPathForLocalPath","getFileTreeRecursively","bulkStorageOperation","path","getStaleFiles","lowerCaseEntityTriplet","JSON5","ForwardedError","lowerCaseEntityTripletInStoragePath","platformPath","getHeadersForFileExtension","limiterFactory"],"mappings":"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"}
1
+ {"version":3,"file":"azureBlobStorage.cjs.js","sources":["../../../src/stages/publish/azureBlobStorage.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { DefaultAzureCredential } from '@azure/identity';\nimport {\n BlobServiceClient,\n ContainerClient,\n StorageSharedKeyCredential,\n} from '@azure/storage-blob';\nimport { Entity, CompoundEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { ForwardedError, toError } from '@backstage/errors';\nimport express from 'express';\nimport JSON5 from 'json5';\nimport limiterFactory from 'p-limit';\nimport { default as path, default as platformPath } from 'node:path';\nimport { Readable } from 'node:stream';\nimport {\n bulkStorageOperation,\n getCloudPathForLocalPath,\n getFileTreeRecursively,\n getHeadersForFileExtension,\n lowerCaseEntityTriplet,\n getStaleFiles,\n lowerCaseEntityTripletInStoragePath,\n} from './helpers';\nimport {\n PublisherBase,\n PublishRequest,\n PublishResponse,\n ReadinessResponse,\n TechDocsMetadata,\n} from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\n// The number of batches that may be ongoing at the same time.\nconst BATCH_CONCURRENCY = 3;\n\nexport class AzureBlobStoragePublish implements PublisherBase {\n private readonly storageClient: BlobServiceClient;\n private readonly containerName: string;\n private readonly legacyPathCasing: boolean;\n private readonly logger: LoggerService;\n\n constructor(options: {\n storageClient: BlobServiceClient;\n containerName: string;\n legacyPathCasing: boolean;\n logger: LoggerService;\n }) {\n this.storageClient = options.storageClient;\n this.containerName = options.containerName;\n this.legacyPathCasing = options.legacyPathCasing;\n this.logger = options.logger;\n }\n\n static fromConfig(config: Config, logger: LoggerService): PublisherBase {\n let storageClient: BlobServiceClient;\n let containerName = '';\n try {\n containerName = config.getString(\n 'techdocs.publisher.azureBlobStorage.containerName',\n );\n } catch (error) {\n throw new Error(\n \"Since techdocs.publisher.type is set to 'azureBlobStorage' in your app config, \" +\n 'techdocs.publisher.azureBlobStorage.containerName is required.',\n );\n }\n\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n\n // Give more priority for connectionString, if configured, return the AzureBlobStoragePublish object here itself\n const connectionStringKey =\n 'techdocs.publisher.azureBlobStorage.connectionString';\n const connectionString = config.getOptionalString(connectionStringKey);\n\n if (connectionString) {\n logger.info(\n `Using '${connectionStringKey}' configuration to create storage client`,\n );\n storageClient = BlobServiceClient.fromConnectionString(connectionString);\n } else {\n let accountName = '';\n try {\n accountName = config.getString(\n 'techdocs.publisher.azureBlobStorage.credentials.accountName',\n );\n } catch (error) {\n throw new Error(\n \"Since techdocs.publisher.type is set to 'azureBlobStorage' in your app config, \" +\n 'techdocs.publisher.azureBlobStorage.credentials.accountName is required.',\n );\n }\n\n // Credentials is an optional config. If missing, default Azure Blob Storage environment variables will be used.\n // https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-app\n const accountKey = config.getOptionalString(\n 'techdocs.publisher.azureBlobStorage.credentials.accountKey',\n );\n\n let credential;\n if (accountKey) {\n credential = new StorageSharedKeyCredential(accountName, accountKey);\n } else {\n credential = new DefaultAzureCredential();\n }\n\n storageClient = new BlobServiceClient(\n `https://${accountName}.blob.core.windows.net`,\n credential,\n );\n }\n\n return new AzureBlobStoragePublish({\n storageClient: storageClient,\n containerName: containerName,\n legacyPathCasing: legacyPathCasing,\n logger: logger,\n });\n }\n\n async getReadiness(): Promise<ReadinessResponse> {\n try {\n const response = await this.storageClient\n .getContainerClient(this.containerName)\n .getProperties();\n\n if (response._response.status === 200) {\n return {\n isAvailable: true,\n };\n }\n\n if (response._response.status >= 400) {\n this.logger.error(\n `Failed to retrieve metadata from ${response._response.request.url} with status code ${response._response.status}.`,\n );\n }\n } catch (e) {\n this.logger.error(\n `from Azure Blob Storage client library: ${toError(e).message}`,\n );\n }\n\n this.logger.error(\n `Could not retrieve metadata about the Azure Blob Storage container ${this.containerName}. ` +\n 'Make sure that the Azure project and container exist and the access key is setup correctly ' +\n 'techdocs.publisher.azureBlobStorage.credentials defined in app config has correct permissions. ' +\n 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',\n );\n\n return { isAvailable: false };\n }\n\n /**\n * Upload all the files from the generated `directory` to the Azure Blob Storage container.\n * Directory structure used in the container is - entityNamespace/entityKind/entityName/index.html\n */\n async publish({\n entity,\n directory,\n }: PublishRequest): Promise<PublishResponse> {\n const objects: string[] = [];\n const useLegacyPathCasing = this.legacyPathCasing;\n\n // First, try to retrieve a list of all individual files currently existing\n const remoteFolder = getCloudPathForLocalPath(\n entity,\n undefined,\n useLegacyPathCasing,\n );\n let existingFiles: string[] = [];\n try {\n existingFiles = await this.getAllBlobsFromContainer({\n prefix: remoteFolder,\n maxPageSize: BATCH_CONCURRENCY,\n });\n } catch (e) {\n this.logger.error(\n `Unable to list files for Entity ${entity.metadata.name}: ${\n toError(e).message\n }`,\n );\n }\n\n // Then, merge new files into the same folder\n let absoluteFilesToUpload;\n let container: ContainerClient;\n try {\n // Remove the absolute path prefix of the source directory\n // Path of all files to upload, relative to the root of the source directory\n // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png']\n absoluteFilesToUpload = await getFileTreeRecursively(directory);\n\n container = this.storageClient.getContainerClient(this.containerName);\n const failedOperations: Error[] = [];\n await bulkStorageOperation(\n async absoluteFilePath => {\n const relativeFilePath = path.normalize(\n path.relative(directory, absoluteFilePath),\n );\n const remotePath = getCloudPathForLocalPath(\n entity,\n relativeFilePath,\n useLegacyPathCasing,\n );\n objects.push(remotePath);\n const response = await container\n .getBlockBlobClient(remotePath)\n .uploadFile(absoluteFilePath);\n\n if (response._response.status >= 400) {\n failedOperations.push(\n new Error(\n `Upload failed for ${absoluteFilePath} with status code ${response._response.status}`,\n ),\n );\n }\n\n return response;\n },\n absoluteFilesToUpload,\n { concurrencyLimit: BATCH_CONCURRENCY },\n );\n\n if (failedOperations.length > 0) {\n throw new Error(\n failedOperations\n .map(r => r.message)\n .filter(Boolean)\n .join(' '),\n );\n }\n\n this.logger.info(\n `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`,\n );\n } catch (e) {\n const errorMessage = `Unable to upload file(s) to Azure. ${e}`;\n this.logger.error(errorMessage);\n throw new Error(errorMessage);\n }\n\n // Last, try to remove the files that were *only* present previously\n try {\n const relativeFilesToUpload = absoluteFilesToUpload.map(\n absoluteFilePath =>\n getCloudPathForLocalPath(\n entity,\n path.relative(directory, absoluteFilePath),\n useLegacyPathCasing,\n ),\n );\n\n const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles);\n\n await bulkStorageOperation(\n async relativeFilePath => {\n return await container.deleteBlob(relativeFilePath);\n },\n staleFiles,\n { concurrencyLimit: BATCH_CONCURRENCY },\n );\n\n this.logger.info(\n `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`,\n );\n } catch (error) {\n const errorMessage = `Unable to delete file(s) from Azure. ${error}`;\n this.logger.error(errorMessage);\n }\n\n return { objects };\n }\n\n async fetchTechDocsMetadata(\n entityName: CompoundEntityRef,\n ): Promise<TechDocsMetadata> {\n const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;\n const entityRootDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n try {\n const techdocsMetadataJson = await new Promise<Buffer>(\n (resolve, reject) => {\n const fileStreamChunks: Array<any> = [];\n this.storageClient\n .getContainerClient(this.containerName)\n .getBlockBlobClient(`${entityRootDir}/techdocs_metadata.json`)\n .download()\n .then(res => {\n const body = res.readableStreamBody;\n if (!body) {\n reject(new Error(`Unable to parse the response data`));\n return;\n }\n body\n .on('error', reject)\n .on('data', chunk => {\n fileStreamChunks.push(chunk);\n })\n .on('end', () => {\n resolve(Buffer.concat(fileStreamChunks));\n });\n })\n .catch(reject);\n },\n );\n\n if (!techdocsMetadataJson) {\n throw new Error(\n `Unable to parse the techdocs metadata file ${entityRootDir}/techdocs_metadata.json.`,\n );\n }\n const techdocsMetadata = JSON5.parse(\n techdocsMetadataJson.toString('utf-8'),\n );\n return techdocsMetadata;\n } catch (e) {\n throw new ForwardedError('TechDocs metadata fetch failed', e);\n }\n }\n\n /**\n * Express route middleware to serve static files on a route in techdocs-backend.\n */\n docsRouter(): express.Handler {\n return (req, res) => {\n // Decode and trim the leading forward slash\n const decodedUri = decodeURI(req.path.replace(/^\\//, ''));\n\n // filePath example - /default/Component/documented-component/index.html\n const filePath = this.legacyPathCasing\n ? decodedUri\n : lowerCaseEntityTripletInStoragePath(decodedUri);\n\n // Files with different extensions (CSS, HTML) need to be served with different headers\n const fileExtension = platformPath.extname(filePath);\n const responseHeaders = getHeadersForFileExtension(fileExtension);\n\n const blobClient = this.storageClient\n .getContainerClient(this.containerName)\n .getBlockBlobClient(filePath);\n\n blobClient\n .download()\n .then(downloadRes => {\n if (!downloadRes.readableStreamBody) {\n throw new Error('Unable to parse the response data');\n }\n const body = downloadRes.readableStreamBody as Readable;\n\n // The client may have disconnected while the download was pending\n if (res.destroyed) {\n body.destroy();\n return;\n }\n\n for (const [headerKey, headerValue] of Object.entries(\n responseHeaders,\n )) {\n res.setHeader(headerKey, headerValue);\n }\n body.pipe(res);\n\n body.on('error', err => {\n this.logger.warn(\n `TechDocs Azure router failed to serve content from container ${this.containerName} at path ${filePath}: ${err.message}`,\n );\n if (!res.headersSent) {\n res.status(404).send('File Not Found');\n } else {\n res.destroy();\n }\n });\n\n // If the client disconnects before the file is fully sent, the piped\n // body is left paused and keeps its connection to Azure open.\n res.on('close', () => {\n if (!body.readableEnded) {\n body.destroy();\n }\n });\n })\n .catch(e => {\n this.logger.warn(\n `TechDocs Azure router failed to serve content from container ${this.containerName} at path ${filePath}: ${e.message}`,\n );\n if (!res.headersSent) {\n res.status(404).send('File Not Found');\n } else {\n res.destroy();\n }\n });\n };\n }\n\n /**\n * A helper function which checks if index.html of an Entity's docs site is available. This\n * can be used to verify if there are any pre-generated docs available to serve.\n */\n hasDocsBeenGenerated(entity: Entity): Promise<boolean> {\n const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;\n const entityRootDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n return this.storageClient\n .getContainerClient(this.containerName)\n .getBlockBlobClient(`${entityRootDir}/index.html`)\n .exists();\n }\n\n protected async renameBlob(\n originalName: string,\n newName: string,\n removeOriginal = false,\n ): Promise<void> {\n const container = this.storageClient.getContainerClient(this.containerName);\n const blob = container.getBlobClient(newName);\n const { url } = container.getBlobClient(originalName);\n const response = await blob.beginCopyFromURL(url);\n await response.pollUntilDone();\n if (removeOriginal) {\n await container.deleteBlob(originalName);\n }\n }\n\n protected async renameBlobToLowerCase(\n originalPath: string,\n removeOriginal: boolean,\n ) {\n let newPath;\n try {\n newPath = lowerCaseEntityTripletInStoragePath(originalPath);\n } catch (e) {\n this.logger.warn(toError(e).message);\n return;\n }\n\n if (originalPath === newPath) return;\n try {\n this.logger.debug(`Migrating ${originalPath}`);\n await this.renameBlob(originalPath, newPath, removeOriginal);\n } catch (e) {\n this.logger.warn(\n `Unable to migrate ${originalPath}: ${toError(e).message}`,\n );\n }\n }\n\n async migrateDocsCase({\n removeOriginal = false,\n concurrency = 25,\n }): Promise<void> {\n const promises = [];\n const limiter = limiterFactory(concurrency);\n const container = this.storageClient.getContainerClient(this.containerName);\n\n for await (const blob of container.listBlobsFlat()) {\n promises.push(\n limiter(\n this.renameBlobToLowerCase.bind(this),\n blob.name,\n removeOriginal,\n ),\n );\n }\n\n await Promise.all(promises);\n }\n\n protected async getAllBlobsFromContainer({\n prefix,\n maxPageSize,\n }: {\n prefix: string;\n maxPageSize: number;\n }): Promise<string[]> {\n const blobs: string[] = [];\n const container = this.storageClient.getContainerClient(this.containerName);\n\n let iterator = container.listBlobsFlat({ prefix }).byPage({ maxPageSize });\n let response = (await iterator.next()).value;\n\n do {\n for (const blob of response?.segment?.blobItems ?? []) {\n blobs.push(blob.name);\n }\n iterator = container\n .listBlobsFlat({ prefix })\n .byPage({ continuationToken: response.continuationToken, maxPageSize });\n response = (await iterator.next()).value;\n } while (response && response.continuationToken);\n\n return blobs;\n }\n}\n"],"names":["BlobServiceClient","StorageSharedKeyCredential","DefaultAzureCredential","toError","getCloudPathForLocalPath","getFileTreeRecursively","bulkStorageOperation","path","getStaleFiles","lowerCaseEntityTriplet","JSON5","ForwardedError","lowerCaseEntityTripletInStoragePath","platformPath","getHeadersForFileExtension","limiterFactory"],"mappings":"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"}
@@ -214,7 +214,13 @@ class GoogleGCSPublish {
214
214
  }
215
215
  const fileExtension = path__default.default.extname(filePath);
216
216
  const responseHeaders = helpers.getHeadersForFileExtension(fileExtension);
217
- this.storageClient.bucket(this.bucketName).file(filePath).createReadStream().on("pipe", () => {
217
+ const body = this.storageClient.bucket(this.bucketName).file(filePath).createReadStream();
218
+ res.on("close", () => {
219
+ if (!body.readableEnded) {
220
+ body.destroy();
221
+ }
222
+ });
223
+ body.on("pipe", () => {
218
224
  res.writeHead(200, responseHeaders);
219
225
  }).on("error", (err) => {
220
226
  this.logger.warn(
@@ -1 +1 @@
1
- {"version":3,"file":"googleStorage.cjs.js","sources":["../../../src/stages/publish/googleStorage.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Entity, CompoundEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { toError } from '@backstage/errors';\nimport {\n File,\n FileExistsResponse,\n Storage,\n StorageOptions,\n} from '@google-cloud/storage';\nimport express from 'express';\nimport JSON5 from 'json5';\nimport path from 'node:path';\nimport { Readable } from 'node:stream';\nimport {\n getFileTreeRecursively,\n getHeadersForFileExtension,\n isValidContentPath,\n lowerCaseEntityTriplet,\n lowerCaseEntityTripletInStoragePath,\n bulkStorageOperation,\n getCloudPathForLocalPath,\n getStaleFiles,\n normalizeExternalStorageRootPath,\n} from './helpers';\nimport { MigrateWriteStream } from './migrations';\nimport {\n PublisherBase,\n PublishRequest,\n PublishResponse,\n ReadinessResponse,\n TechDocsMetadata,\n} from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport class GoogleGCSPublish implements PublisherBase {\n private readonly storageClient: Storage;\n private readonly bucketName: string;\n private readonly legacyPathCasing: boolean;\n private readonly logger: LoggerService;\n private readonly bucketRootPath: string;\n\n constructor(options: {\n storageClient: Storage;\n bucketName: string;\n legacyPathCasing: boolean;\n logger: LoggerService;\n bucketRootPath: string;\n }) {\n this.storageClient = options.storageClient;\n this.bucketName = options.bucketName;\n this.legacyPathCasing = options.legacyPathCasing;\n this.logger = options.logger;\n this.bucketRootPath = options.bucketRootPath;\n }\n\n static fromConfig(\n config: Config,\n logger: LoggerService,\n options?: StorageOptions,\n ): PublisherBase {\n let bucketName = '';\n try {\n bucketName = config.getString('techdocs.publisher.googleGcs.bucketName');\n } catch (error) {\n throw new Error(\n \"Since techdocs.publisher.type is set to 'googleGcs' in your app config, \" +\n 'techdocs.publisher.googleGcs.bucketName is required.',\n );\n }\n\n const bucketRootPath = normalizeExternalStorageRootPath(\n config.getOptionalString('techdocs.publisher.googleGcs.bucketRootPath') ||\n '',\n );\n\n // Credentials is an optional config. If missing, default GCS environment variables will be used.\n // Read more here https://cloud.google.com/docs/authentication/production\n const credentials = config.getOptionalString(\n 'techdocs.publisher.googleGcs.credentials',\n );\n const projectId = config.getOptionalString(\n 'techdocs.publisher.googleGcs.projectId',\n );\n let credentialsJson: any = {};\n if (credentials) {\n try {\n credentialsJson = JSON.parse(credentials);\n } catch (err) {\n throw new Error(\n 'Error in parsing techdocs.publisher.googleGcs.credentials config to JSON.',\n );\n }\n }\n\n const clientOpts: StorageOptions = options ?? {};\n if (projectId) {\n clientOpts.projectId = projectId;\n }\n\n const storageClient = new Storage({\n ...(credentials && {\n projectId: credentialsJson.project_id,\n credentials: credentialsJson,\n }),\n ...clientOpts,\n });\n\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n\n return new GoogleGCSPublish({\n storageClient,\n bucketName,\n legacyPathCasing,\n logger,\n bucketRootPath,\n });\n }\n\n /**\n * Check if the defined bucket exists. Being able to connect means the configuration is good\n * and the storage client will work.\n */\n async getReadiness(): Promise<ReadinessResponse> {\n try {\n await this.storageClient.bucket(this.bucketName).getMetadata();\n this.logger.info(\n `Successfully connected to the GCS bucket ${this.bucketName}.`,\n );\n\n return {\n isAvailable: true,\n };\n } catch (err) {\n this.logger.error(\n `Could not retrieve metadata about the GCS bucket ${this.bucketName}. ` +\n 'Make sure the bucket exists. Also make sure that authentication is setup either by explicitly defining ' +\n 'techdocs.publisher.googleGcs.credentials in app config or by using environment variables. ' +\n 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',\n );\n this.logger.error(`from GCS client library: ${toError(err).message}`);\n\n return { isAvailable: false };\n }\n }\n\n /**\n * Upload all the files from the generated `directory` to the GCS bucket.\n * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html\n */\n async publish({\n entity,\n directory,\n }: PublishRequest): Promise<PublishResponse> {\n const objects: string[] = [];\n const useLegacyPathCasing = this.legacyPathCasing;\n const bucket = this.storageClient.bucket(this.bucketName);\n const bucketRootPath = this.bucketRootPath;\n\n // First, try to retrieve a list of all individual files currently existing\n let existingFiles: string[] = [];\n try {\n const remoteFolder = getCloudPathForLocalPath(\n entity,\n undefined,\n useLegacyPathCasing,\n bucketRootPath,\n );\n existingFiles = await this.getFilesForFolder(remoteFolder);\n } catch (e) {\n this.logger.error(\n `Unable to list files for Entity ${entity.metadata.name}: ${\n toError(e).message\n }`,\n );\n }\n\n // Then, merge new files into the same folder\n let absoluteFilesToUpload;\n try {\n // Remove the absolute path prefix of the source directory\n // Path of all files to upload, relative to the root of the source directory\n // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png']\n absoluteFilesToUpload = await getFileTreeRecursively(directory);\n\n await bulkStorageOperation(\n async absoluteFilePath => {\n const relativeFilePath = path.relative(directory, absoluteFilePath);\n const destination = getCloudPathForLocalPath(\n entity,\n relativeFilePath,\n useLegacyPathCasing,\n bucketRootPath,\n );\n objects.push(destination);\n return await bucket.upload(absoluteFilePath, { destination });\n },\n absoluteFilesToUpload,\n { concurrencyLimit: 10 },\n );\n\n this.logger.info(\n `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`,\n );\n } catch (e) {\n const errorMessage = `Unable to upload file(s) to Google Cloud Storage. ${e}`;\n this.logger.error(errorMessage);\n throw new Error(errorMessage);\n }\n\n // Last, try to remove the files that were *only* present previously\n try {\n const relativeFilesToUpload = absoluteFilesToUpload.map(\n absoluteFilePath =>\n getCloudPathForLocalPath(\n entity,\n path.relative(directory, absoluteFilePath),\n useLegacyPathCasing,\n bucketRootPath,\n ),\n );\n const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles);\n\n await bulkStorageOperation(\n async relativeFilePath => {\n return await bucket.file(relativeFilePath).delete();\n },\n staleFiles,\n { concurrencyLimit: 10 },\n );\n\n this.logger.info(\n `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`,\n );\n } catch (error) {\n const errorMessage = `Unable to delete file(s) from Google Cloud Storage. ${error}`;\n this.logger.error(errorMessage);\n }\n\n return { objects };\n }\n\n fetchTechDocsMetadata(\n entityName: CompoundEntityRef,\n ): Promise<TechDocsMetadata> {\n return new Promise((resolve, reject) => {\n const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;\n const entityDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);\n if (!isValidContentPath(this.bucketRootPath, entityRootDir)) {\n this.logger.error(\n `Invalid content path found while fetching TechDocs metadata: ${entityRootDir}`,\n );\n reject(new Error(`Metadata Not Found`));\n }\n\n const fileStreamChunks: Array<any> = [];\n this.storageClient\n .bucket(this.bucketName)\n .file(`${entityRootDir}/techdocs_metadata.json`)\n .createReadStream()\n .on('error', err => {\n this.logger.error(err.message);\n reject(err);\n })\n .on('data', chunk => {\n fileStreamChunks.push(chunk);\n })\n .on('end', () => {\n const techdocsMetadataJson =\n Buffer.concat(fileStreamChunks).toString('utf-8');\n resolve(JSON5.parse(techdocsMetadataJson));\n });\n });\n }\n\n /**\n * Express route middleware to serve static files on a route in techdocs-backend.\n */\n docsRouter(): express.Handler {\n return (req, res) => {\n const decodedUri = decodeURI(req.path.replace(/^\\//, ''));\n\n // filePath example - /default/component/documented-component/index.html\n const filePathNoRoot = this.legacyPathCasing\n ? decodedUri\n : lowerCaseEntityTripletInStoragePath(decodedUri);\n\n // Prepend the root path to the relative file path\n const filePath = path.posix.join(this.bucketRootPath, filePathNoRoot);\n if (!isValidContentPath(this.bucketRootPath, filePath)) {\n this.logger.error(\n `Attempted to fetch TechDocs content for a file outside of the bucket root: ${filePathNoRoot}`,\n );\n res.status(404).send('File Not Found');\n return;\n }\n\n // Files with different extensions (CSS, HTML) need to be served with different headers\n const fileExtension = path.extname(filePath);\n const responseHeaders = getHeadersForFileExtension(fileExtension);\n\n // Pipe file chunks directly from storage to client.\n this.storageClient\n .bucket(this.bucketName)\n .file(filePath)\n .createReadStream()\n .on('pipe', () => {\n res.writeHead(200, responseHeaders);\n })\n .on('error', err => {\n this.logger.warn(\n `TechDocs Google GCS router failed to serve content from bucket ${this.bucketName} at path ${filePath}: ${err.message}`,\n );\n // Send a 404 with a meaningful message if possible.\n if (!res.headersSent) {\n res.status(404).send('File Not Found');\n } else {\n res.destroy();\n }\n })\n .pipe(res);\n };\n }\n\n /**\n * A helper function which checks if index.html of an Entity's docs site is available. This\n * can be used to verify if there are any pre-generated docs available to serve.\n */\n async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {\n return new Promise(resolve => {\n const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;\n const entityDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);\n if (!isValidContentPath(this.bucketRootPath, entityRootDir)) {\n this.logger.error(\n `Invalid content path found while checking if docs have been generated: ${entityRootDir}`,\n );\n resolve(false);\n }\n\n this.storageClient\n .bucket(this.bucketName)\n .file(`${entityRootDir}/index.html`)\n .exists()\n .then((response: FileExistsResponse) => {\n resolve(response[0]);\n })\n .catch(() => {\n resolve(false);\n });\n });\n }\n\n migrateDocsCase({ removeOriginal = false, concurrency = 25 }): Promise<void> {\n return new Promise((resolve, reject) => {\n // Iterate through every file in the root of the publisher.\n const allFileMetadata: Readable = this.storageClient\n .bucket(this.bucketName)\n .getFilesStream();\n const migrateFiles = new MigrateWriteStream(\n this.logger,\n removeOriginal,\n concurrency,\n );\n migrateFiles.on('finish', resolve).on('error', reject);\n allFileMetadata.pipe(migrateFiles).on('error', error => {\n migrateFiles.destroy();\n reject(error);\n });\n });\n }\n\n private getFilesForFolder(folder: string): Promise<string[]> {\n const fileMetadataStream: Readable = this.storageClient\n .bucket(this.bucketName)\n .getFilesStream({ prefix: folder });\n\n return new Promise((resolve, reject) => {\n const files: string[] = [];\n\n fileMetadataStream.on('error', error => {\n // push file to file array\n reject(error);\n });\n\n fileMetadataStream.on('data', (file: File) => {\n // push file to file array\n files.push(file.name);\n });\n\n fileMetadataStream.on('end', () => {\n // resolve promise\n resolve(files);\n });\n });\n }\n}\n"],"names":["normalizeExternalStorageRootPath","Storage","toError","getCloudPathForLocalPath","getFileTreeRecursively","bulkStorageOperation","path","getStaleFiles","lowerCaseEntityTriplet","isValidContentPath","JSON5","lowerCaseEntityTripletInStoragePath","getHeadersForFileExtension","MigrateWriteStream"],"mappings":"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"}
1
+ {"version":3,"file":"googleStorage.cjs.js","sources":["../../../src/stages/publish/googleStorage.ts"],"sourcesContent":["/*\n * Copyright 2020 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\nimport { Entity, CompoundEntityRef } from '@backstage/catalog-model';\nimport { Config } from '@backstage/config';\nimport { toError } from '@backstage/errors';\nimport {\n File,\n FileExistsResponse,\n Storage,\n StorageOptions,\n} from '@google-cloud/storage';\nimport express from 'express';\nimport JSON5 from 'json5';\nimport path from 'node:path';\nimport { Readable } from 'node:stream';\nimport {\n getFileTreeRecursively,\n getHeadersForFileExtension,\n isValidContentPath,\n lowerCaseEntityTriplet,\n lowerCaseEntityTripletInStoragePath,\n bulkStorageOperation,\n getCloudPathForLocalPath,\n getStaleFiles,\n normalizeExternalStorageRootPath,\n} from './helpers';\nimport { MigrateWriteStream } from './migrations';\nimport {\n PublisherBase,\n PublishRequest,\n PublishResponse,\n ReadinessResponse,\n TechDocsMetadata,\n} from './types';\nimport { LoggerService } from '@backstage/backend-plugin-api';\n\nexport class GoogleGCSPublish implements PublisherBase {\n private readonly storageClient: Storage;\n private readonly bucketName: string;\n private readonly legacyPathCasing: boolean;\n private readonly logger: LoggerService;\n private readonly bucketRootPath: string;\n\n constructor(options: {\n storageClient: Storage;\n bucketName: string;\n legacyPathCasing: boolean;\n logger: LoggerService;\n bucketRootPath: string;\n }) {\n this.storageClient = options.storageClient;\n this.bucketName = options.bucketName;\n this.legacyPathCasing = options.legacyPathCasing;\n this.logger = options.logger;\n this.bucketRootPath = options.bucketRootPath;\n }\n\n static fromConfig(\n config: Config,\n logger: LoggerService,\n options?: StorageOptions,\n ): PublisherBase {\n let bucketName = '';\n try {\n bucketName = config.getString('techdocs.publisher.googleGcs.bucketName');\n } catch (error) {\n throw new Error(\n \"Since techdocs.publisher.type is set to 'googleGcs' in your app config, \" +\n 'techdocs.publisher.googleGcs.bucketName is required.',\n );\n }\n\n const bucketRootPath = normalizeExternalStorageRootPath(\n config.getOptionalString('techdocs.publisher.googleGcs.bucketRootPath') ||\n '',\n );\n\n // Credentials is an optional config. If missing, default GCS environment variables will be used.\n // Read more here https://cloud.google.com/docs/authentication/production\n const credentials = config.getOptionalString(\n 'techdocs.publisher.googleGcs.credentials',\n );\n const projectId = config.getOptionalString(\n 'techdocs.publisher.googleGcs.projectId',\n );\n let credentialsJson: any = {};\n if (credentials) {\n try {\n credentialsJson = JSON.parse(credentials);\n } catch (err) {\n throw new Error(\n 'Error in parsing techdocs.publisher.googleGcs.credentials config to JSON.',\n );\n }\n }\n\n const clientOpts: StorageOptions = options ?? {};\n if (projectId) {\n clientOpts.projectId = projectId;\n }\n\n const storageClient = new Storage({\n ...(credentials && {\n projectId: credentialsJson.project_id,\n credentials: credentialsJson,\n }),\n ...clientOpts,\n });\n\n const legacyPathCasing =\n config.getOptionalBoolean(\n 'techdocs.legacyUseCaseSensitiveTripletPaths',\n ) || false;\n\n return new GoogleGCSPublish({\n storageClient,\n bucketName,\n legacyPathCasing,\n logger,\n bucketRootPath,\n });\n }\n\n /**\n * Check if the defined bucket exists. Being able to connect means the configuration is good\n * and the storage client will work.\n */\n async getReadiness(): Promise<ReadinessResponse> {\n try {\n await this.storageClient.bucket(this.bucketName).getMetadata();\n this.logger.info(\n `Successfully connected to the GCS bucket ${this.bucketName}.`,\n );\n\n return {\n isAvailable: true,\n };\n } catch (err) {\n this.logger.error(\n `Could not retrieve metadata about the GCS bucket ${this.bucketName}. ` +\n 'Make sure the bucket exists. Also make sure that authentication is setup either by explicitly defining ' +\n 'techdocs.publisher.googleGcs.credentials in app config or by using environment variables. ' +\n 'Refer to https://backstage.io/docs/features/techdocs/using-cloud-storage',\n );\n this.logger.error(`from GCS client library: ${toError(err).message}`);\n\n return { isAvailable: false };\n }\n }\n\n /**\n * Upload all the files from the generated `directory` to the GCS bucket.\n * Directory structure used in the bucket is - entityNamespace/entityKind/entityName/index.html\n */\n async publish({\n entity,\n directory,\n }: PublishRequest): Promise<PublishResponse> {\n const objects: string[] = [];\n const useLegacyPathCasing = this.legacyPathCasing;\n const bucket = this.storageClient.bucket(this.bucketName);\n const bucketRootPath = this.bucketRootPath;\n\n // First, try to retrieve a list of all individual files currently existing\n let existingFiles: string[] = [];\n try {\n const remoteFolder = getCloudPathForLocalPath(\n entity,\n undefined,\n useLegacyPathCasing,\n bucketRootPath,\n );\n existingFiles = await this.getFilesForFolder(remoteFolder);\n } catch (e) {\n this.logger.error(\n `Unable to list files for Entity ${entity.metadata.name}: ${\n toError(e).message\n }`,\n );\n }\n\n // Then, merge new files into the same folder\n let absoluteFilesToUpload;\n try {\n // Remove the absolute path prefix of the source directory\n // Path of all files to upload, relative to the root of the source directory\n // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png']\n absoluteFilesToUpload = await getFileTreeRecursively(directory);\n\n await bulkStorageOperation(\n async absoluteFilePath => {\n const relativeFilePath = path.relative(directory, absoluteFilePath);\n const destination = getCloudPathForLocalPath(\n entity,\n relativeFilePath,\n useLegacyPathCasing,\n bucketRootPath,\n );\n objects.push(destination);\n return await bucket.upload(absoluteFilePath, { destination });\n },\n absoluteFilesToUpload,\n { concurrencyLimit: 10 },\n );\n\n this.logger.info(\n `Successfully uploaded all the generated files for Entity ${entity.metadata.name}. Total number of files: ${absoluteFilesToUpload.length}`,\n );\n } catch (e) {\n const errorMessage = `Unable to upload file(s) to Google Cloud Storage. ${e}`;\n this.logger.error(errorMessage);\n throw new Error(errorMessage);\n }\n\n // Last, try to remove the files that were *only* present previously\n try {\n const relativeFilesToUpload = absoluteFilesToUpload.map(\n absoluteFilePath =>\n getCloudPathForLocalPath(\n entity,\n path.relative(directory, absoluteFilePath),\n useLegacyPathCasing,\n bucketRootPath,\n ),\n );\n const staleFiles = getStaleFiles(relativeFilesToUpload, existingFiles);\n\n await bulkStorageOperation(\n async relativeFilePath => {\n return await bucket.file(relativeFilePath).delete();\n },\n staleFiles,\n { concurrencyLimit: 10 },\n );\n\n this.logger.info(\n `Successfully deleted stale files for Entity ${entity.metadata.name}. Total number of files: ${staleFiles.length}`,\n );\n } catch (error) {\n const errorMessage = `Unable to delete file(s) from Google Cloud Storage. ${error}`;\n this.logger.error(errorMessage);\n }\n\n return { objects };\n }\n\n fetchTechDocsMetadata(\n entityName: CompoundEntityRef,\n ): Promise<TechDocsMetadata> {\n return new Promise((resolve, reject) => {\n const entityTriplet = `${entityName.namespace}/${entityName.kind}/${entityName.name}`;\n const entityDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);\n if (!isValidContentPath(this.bucketRootPath, entityRootDir)) {\n this.logger.error(\n `Invalid content path found while fetching TechDocs metadata: ${entityRootDir}`,\n );\n reject(new Error(`Metadata Not Found`));\n }\n\n const fileStreamChunks: Array<any> = [];\n this.storageClient\n .bucket(this.bucketName)\n .file(`${entityRootDir}/techdocs_metadata.json`)\n .createReadStream()\n .on('error', err => {\n this.logger.error(err.message);\n reject(err);\n })\n .on('data', chunk => {\n fileStreamChunks.push(chunk);\n })\n .on('end', () => {\n const techdocsMetadataJson =\n Buffer.concat(fileStreamChunks).toString('utf-8');\n resolve(JSON5.parse(techdocsMetadataJson));\n });\n });\n }\n\n /**\n * Express route middleware to serve static files on a route in techdocs-backend.\n */\n docsRouter(): express.Handler {\n return (req, res) => {\n const decodedUri = decodeURI(req.path.replace(/^\\//, ''));\n\n // filePath example - /default/component/documented-component/index.html\n const filePathNoRoot = this.legacyPathCasing\n ? decodedUri\n : lowerCaseEntityTripletInStoragePath(decodedUri);\n\n // Prepend the root path to the relative file path\n const filePath = path.posix.join(this.bucketRootPath, filePathNoRoot);\n if (!isValidContentPath(this.bucketRootPath, filePath)) {\n this.logger.error(\n `Attempted to fetch TechDocs content for a file outside of the bucket root: ${filePathNoRoot}`,\n );\n res.status(404).send('File Not Found');\n return;\n }\n\n // Files with different extensions (CSS, HTML) need to be served with different headers\n const fileExtension = path.extname(filePath);\n const responseHeaders = getHeadersForFileExtension(fileExtension);\n\n const body = this.storageClient\n .bucket(this.bucketName)\n .file(filePath)\n .createReadStream();\n\n // If the client disconnects before the file is fully sent, the piped\n // body is left paused and keeps its connection to GCS open.\n res.on('close', () => {\n if (!body.readableEnded) {\n body.destroy();\n }\n });\n\n // Pipe file chunks directly from storage to client.\n body\n .on('pipe', () => {\n res.writeHead(200, responseHeaders);\n })\n .on('error', err => {\n this.logger.warn(\n `TechDocs Google GCS router failed to serve content from bucket ${this.bucketName} at path ${filePath}: ${err.message}`,\n );\n // Send a 404 with a meaningful message if possible.\n if (!res.headersSent) {\n res.status(404).send('File Not Found');\n } else {\n res.destroy();\n }\n })\n .pipe(res);\n };\n }\n\n /**\n * A helper function which checks if index.html of an Entity's docs site is available. This\n * can be used to verify if there are any pre-generated docs available to serve.\n */\n async hasDocsBeenGenerated(entity: Entity): Promise<boolean> {\n return new Promise(resolve => {\n const entityTriplet = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;\n const entityDir = this.legacyPathCasing\n ? entityTriplet\n : lowerCaseEntityTriplet(entityTriplet);\n\n const entityRootDir = path.posix.join(this.bucketRootPath, entityDir);\n if (!isValidContentPath(this.bucketRootPath, entityRootDir)) {\n this.logger.error(\n `Invalid content path found while checking if docs have been generated: ${entityRootDir}`,\n );\n resolve(false);\n }\n\n this.storageClient\n .bucket(this.bucketName)\n .file(`${entityRootDir}/index.html`)\n .exists()\n .then((response: FileExistsResponse) => {\n resolve(response[0]);\n })\n .catch(() => {\n resolve(false);\n });\n });\n }\n\n migrateDocsCase({ removeOriginal = false, concurrency = 25 }): Promise<void> {\n return new Promise((resolve, reject) => {\n // Iterate through every file in the root of the publisher.\n const allFileMetadata: Readable = this.storageClient\n .bucket(this.bucketName)\n .getFilesStream();\n const migrateFiles = new MigrateWriteStream(\n this.logger,\n removeOriginal,\n concurrency,\n );\n migrateFiles.on('finish', resolve).on('error', reject);\n allFileMetadata.pipe(migrateFiles).on('error', error => {\n migrateFiles.destroy();\n reject(error);\n });\n });\n }\n\n private getFilesForFolder(folder: string): Promise<string[]> {\n const fileMetadataStream: Readable = this.storageClient\n .bucket(this.bucketName)\n .getFilesStream({ prefix: folder });\n\n return new Promise((resolve, reject) => {\n const files: string[] = [];\n\n fileMetadataStream.on('error', error => {\n // push file to file array\n reject(error);\n });\n\n fileMetadataStream.on('data', (file: File) => {\n // push file to file array\n files.push(file.name);\n });\n\n fileMetadataStream.on('end', () => {\n // resolve promise\n resolve(files);\n });\n });\n }\n}\n"],"names":["normalizeExternalStorageRootPath","Storage","toError","getCloudPathForLocalPath","getFileTreeRecursively","bulkStorageOperation","path","getStaleFiles","lowerCaseEntityTriplet","isValidContentPath","JSON5","lowerCaseEntityTripletInStoragePath","getHeadersForFileExtension","MigrateWriteStream"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ 'use strict';
2
+
3
+ const TECHDOCS_ENGINE_CATALOG_ANNOTATION_KEY = "backstage.io/techdocs-engine";
4
+
5
+ exports.TECHDOCS_ENGINE_CATALOG_ANNOTATION_KEY = TECHDOCS_ENGINE_CATALOG_ANNOTATION_KEY;
6
+ //# sourceMappingURL=techdocsTypes.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"techdocsTypes.cjs.js","sources":["../src/techdocsTypes.ts"],"sourcesContent":["/*\n * Copyright 2021 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { Entity } from '@backstage/catalog-model';\nimport { IndexableDocument } from '@backstage/plugin-search-common';\n\n/**\n * TechDocs indexable document interface\n * @public\n */\nexport interface TechDocsDocument extends IndexableDocument {\n /**\n * Entity kind\n */\n kind: string;\n /**\n * Entity metadata namespace\n */\n namespace: string;\n /**\n * Entity metadata name\n */\n name: string;\n /**\n * Entity lifecycle\n */\n lifecycle: string;\n /**\n * Entity owner\n */\n owner: string;\n /**\n * Entity path\n */\n path: string;\n}\n\n/**\n * A strategy for when to build TechDocs locally, and when to skip building TechDocs (allowing for an external build)\n *\n * @public\n */\nexport interface DocsBuildStrategy {\n shouldBuild(params: { entity: Entity }): Promise<boolean>;\n}\n\n/**\n * A catalog annotation key indicating which TechDoc Generation Engine to use\n *\n * @public\n */\nexport const TECHDOCS_ENGINE_CATALOG_ANNOTATION_KEY =\n 'backstage.io/techdocs-engine';\n"],"names":[],"mappings":";;AAgEO,MAAM,sCAAA,GACX;;"}
@@ -0,0 +1,100 @@
1
+ import { LoggerService } from '@backstage/backend-plugin-api';
2
+ import { SpawnOptionsWithoutStdio } from 'node:child_process';
3
+ import * as yaml from 'js-yaml';
4
+ import { Writable } from 'node:stream';
5
+
6
+ /** @alpha */
7
+ type RunCommandOptions = {
8
+ /** command to run */
9
+ command: string;
10
+ /** arguments to pass the command */
11
+ args: string[];
12
+ /** options to pass to spawn */
13
+ options: SpawnOptionsWithoutStdio;
14
+ /** stream to capture stdout and stderr output */
15
+ logStream?: Writable;
16
+ };
17
+ /**
18
+ * Run a command in a sub-process, normally a shell command.
19
+ * @alpha
20
+ */
21
+ declare const runCommand: ({ command, args, options, logStream, }: RunCommandOptions) => Promise<void>;
22
+ /**
23
+ * Preserve YAML merge keys and MkDocs-specific tags while inspecting config.
24
+ *
25
+ * @alpha
26
+ */
27
+ declare const MKDOCS_SCHEMA: yaml.Schema;
28
+ /**
29
+ * Finds and loads the contents of an mkdocs.yml, mkdocs.yaml file, a file
30
+ * with a specified name or an ad-hoc created file with minimal config.
31
+ * @public
32
+ *
33
+ * @param inputDir - base dir to be searched for either an mkdocs.yml or mkdocs.yaml file.
34
+ * @param options - name: default mkdocs site_name to be used with a ad hoc file default value is "Documentation Site"
35
+ * mkdocsConfigFileName (optional): a non-default file name to be used as the config
36
+ */
37
+ declare const getMkdocsYml: (inputDir: string, options?: {
38
+ name?: string;
39
+ mkdocsConfigFileName?: string;
40
+ }) => Promise<{
41
+ path: string;
42
+ content: string;
43
+ configIsTemporary: boolean;
44
+ }>;
45
+ /**
46
+ * Validating mkdocs config file for incorrect/insecure values
47
+ * Throws on invalid configs
48
+ *
49
+ * @param inputDir - base dir to be used as a docs_dir path validity check
50
+ * @param mkdocsYmlFileString - The string contents of the loaded
51
+ * mkdocs.yml or equivalent of a docs site
52
+ * @returns the parsed docs_dir or undefined
53
+ * @alpha
54
+ */
55
+ declare const validateMkdocsYaml: (inputDir: string, mkdocsYmlFileString: string) => Promise<string | undefined>;
56
+ /**
57
+ * Validates that the input directory doesn't contain symlinks pointing outside
58
+ * of it. This prevents path traversal attacks where malicious symlinks could be
59
+ * used to read arbitrary files from the host filesystem.
60
+ *
61
+ * The whole input directory is checked rather than only the docs directory,
62
+ * because MkDocs extensions can read files from anywhere in the input directory.
63
+ *
64
+ * @param inputDir - The input directory to validate (absolute path)
65
+ * @alpha
66
+ */
67
+ declare const validateInputDirectory: (inputDir: string) => Promise<void>;
68
+ /**
69
+ * Update docs/index.md file before TechDocs generator uses it to generate docs site,
70
+ * falling back to docs/README.md or README.md in case a default docs/index.md
71
+ * is not provided.
72
+ * @alpha
73
+ */
74
+ declare const patchIndexPreBuild: ({ inputDir, logger, docsDir, }: {
75
+ inputDir: string;
76
+ logger: LoggerService;
77
+ docsDir?: string;
78
+ }) => Promise<void>;
79
+ /**
80
+ * Create or update the techdocs_metadata.json. Values initialized/updated are:
81
+ * - The build_timestamp (now)
82
+ * - The list of files generated
83
+ *
84
+ * @param techdocsMetadataPath - File path to techdocs_metadata.json
85
+ * @alpha
86
+ */
87
+ declare const createOrUpdateMetadata: (techdocsMetadataPath: string, logger: LoggerService, engine?: string) => Promise<void>;
88
+ /**
89
+ * Update the techdocs_metadata.json to add etag of the prepared tree (e.g. commit SHA or actual Etag of the resource).
90
+ * This is helpful to check if a TechDocs site in storage has gone outdated, without maintaining an in-memory build info
91
+ * per Backstage instance.
92
+ *
93
+ * @param techdocsMetadataPath - File path to techdocs_metadata.json
94
+ * @param etag - The ETag to use
95
+ * @alpha
96
+ */
97
+ declare const storeEtagMetadata: (techdocsMetadataPath: string, etag: string) => Promise<void>;
98
+
99
+ export { MKDOCS_SCHEMA as M, validateMkdocsYaml as a, createOrUpdateMetadata as c, getMkdocsYml as g, patchIndexPreBuild as p, runCommand as r, storeEtagMetadata as s, validateInputDirectory as v };
100
+ export type { RunCommandOptions as R };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-techdocs-node",
3
- "version": "2.0.3-next.2",
3
+ "version": "2.2.0-next.0",
4
4
  "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli",
5
5
  "backstage": {
6
6
  "role": "node-library",
@@ -14,9 +14,7 @@
14
14
  ]
15
15
  },
16
16
  "publishConfig": {
17
- "access": "public",
18
- "main": "dist/index.cjs.js",
19
- "types": "dist/index.d.ts"
17
+ "access": "public"
20
18
  },
21
19
  "keywords": [
22
20
  "techdocs",
@@ -32,8 +30,31 @@
32
30
  "directory": "plugins/techdocs-node"
33
31
  },
34
32
  "license": "Apache-2.0",
35
- "main": "dist/index.cjs.js",
36
- "types": "dist/index.d.ts",
33
+ "exports": {
34
+ ".": {
35
+ "require": "./dist/index.cjs.js",
36
+ "types": "./dist/index.d.ts",
37
+ "default": "./dist/index.cjs.js"
38
+ },
39
+ "./alpha": {
40
+ "require": "./dist/alpha.cjs.js",
41
+ "types": "./dist/alpha.d.ts",
42
+ "default": "./dist/alpha.cjs.js"
43
+ },
44
+ "./package.json": "./package.json"
45
+ },
46
+ "main": "./dist/index.cjs.js",
47
+ "types": "./dist/index.d.ts",
48
+ "typesVersions": {
49
+ "*": {
50
+ "alpha": [
51
+ "dist/alpha.d.ts"
52
+ ],
53
+ "package.json": [
54
+ "package.json"
55
+ ]
56
+ }
57
+ },
37
58
  "files": [
38
59
  "dist"
39
60
  ],
@@ -53,18 +74,19 @@
53
74
  "@aws-sdk/types": "^3.347.0",
54
75
  "@azure/identity": "^4.0.0",
55
76
  "@azure/storage-blob": "^12.5.0",
56
- "@backstage/backend-plugin-api": "1.11.0-next.1",
57
- "@backstage/catalog-model": "1.10.2-next.0",
58
- "@backstage/config": "1.3.10-next.0",
59
- "@backstage/errors": "1.3.2-next.0",
60
- "@backstage/integration": "2.1.3-next.1",
61
- "@backstage/integration-aws-node": "0.2.3-next.0",
62
- "@backstage/plugin-search-common": "1.2.26-next.0",
63
- "@backstage/plugin-techdocs-common": "0.1.2-next.0",
77
+ "@backstage/backend-plugin-api": "1.11.1-next.0",
78
+ "@backstage/catalog-model": "1.10.2",
79
+ "@backstage/config": "1.3.10",
80
+ "@backstage/errors": "1.3.2",
81
+ "@backstage/integration": "2.1.3",
82
+ "@backstage/integration-aws-node": "0.2.3",
83
+ "@backstage/plugin-search-common": "1.2.26",
84
+ "@backstage/plugin-techdocs-common": "0.1.2",
64
85
  "@google-cloud/storage": "^7.0.0",
65
86
  "@smithy/node-http-handler": "^3.0.0",
66
87
  "@trendyol-js/openstack-swift-sdk": "^0.0.7",
67
88
  "@types/express": "^4.17.6",
89
+ "@types/js-yaml": "^4.0.0",
68
90
  "dockerode": "^4.0.0",
69
91
  "express": "^4.22.0",
70
92
  "fs-extra": "^11.2.0",
@@ -78,20 +100,12 @@
78
100
  "winston": "^3.2.1"
79
101
  },
80
102
  "devDependencies": {
81
- "@backstage/backend-test-utils": "1.11.8-next.1",
82
- "@backstage/cli": "0.36.7-next.0",
103
+ "@backstage/backend-test-utils": "1.11.9-next.0",
104
+ "@backstage/cli": "0.36.8-next.0",
83
105
  "@types/fs-extra": "^11.0.0",
84
- "@types/js-yaml": "^4.0.0",
85
106
  "@types/mime-types": "^2.1.0",
86
107
  "@types/recursive-readdir": "^2.2.0",
87
108
  "@types/supertest": "^2.0.8",
88
109
  "supertest": "^7.0.0"
89
- },
90
- "typesVersions": {
91
- "*": {
92
- "package.json": [
93
- "package.json"
94
- ]
95
- }
96
110
  }
97
111
  }