diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts index b956b4309a..b546494118 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -15,7 +15,28 @@ */ import type { S3 as S3Types } from 'aws-sdk'; import { EventEmitter } from 'events'; -import fs from 'fs'; +import fs from 'fs-extra'; +import os from 'os'; +import path from 'path'; + +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + +/** + * @param Key Contains either / or \ as file separator depending upon OS. + */ +const checkFileExists = async (Key: string): Promise => { + // Key will always have / as file separator irrespective of OS since S3 expects /. + // Normalize Key to OS specific path before checking if file exists. + const relativeFilePath = Key.split(path.posix.sep).join(path.sep); + const filePath = path.join(rootDir, Key); + + try { + await fs.access(filePath, fs.constants.F_OK); + return true; + } catch (err) { + return false; + } +}; export class S3 { private readonly options; @@ -26,22 +47,27 @@ export class S3 { headObject({ Key }: { Key: string }) { return { - promise: () => this.checkFileExists(Key), + promise: async () => { + if (!(await checkFileExists(Key))) { + throw new Error('File does not exist'); + } + }, }; } getObject({ Key }: { Key: string }) { + const filePath = path.join(rootDir, Key); return { - promise: () => this.checkFileExists(Key), + promise: () => checkFileExists(filePath), createReadStream: () => { const emitter = new EventEmitter(); process.nextTick(() => { - if (fs.existsSync(Key)) { - emitter.emit('data', Buffer.from(fs.readFileSync(Key))); + if (fs.existsSync(filePath)) { + emitter.emit('data', Buffer.from(fs.readFileSync(filePath))); } else { emitter.emit( 'error', - new Error(`The file ${Key} does not exist !`), + new Error(`The file ${filePath} does not exist !`), ); } emitter.emit('end'); @@ -51,16 +77,6 @@ export class S3 { }; } - checkFileExists(Key: string) { - return new Promise((resolve, reject) => { - if (fs.existsSync(Key)) { - resolve(''); - } else { - reject({ message: 'The object does not exist !' }); - } - }); - } - headBucket() { return new Promise(resolve => { resolve(''); @@ -70,9 +86,9 @@ export class S3 { upload({ Key }: { Key: string }) { return { promise: () => - new Promise((resolve, reject) => { - if (!fs.existsSync(Key)) { - reject(''); + new Promise(async (resolve, reject) => { + if (!(await checkFileExists(Key))) { + reject(`The file ${Key} does not exist`); } else { resolve(''); } diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 9376e74e92..bf029dd7db 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -16,7 +16,8 @@ import type { Entity, EntityName } from '@backstage/catalog-model'; import { ConfigReader } from '@backstage/config'; import mockFs from 'mock-fs'; -import path from 'path'; +import * as os from 'os'; +import * as path from 'path'; import * as winston from 'winston'; import { AwsS3Publish } from './awsS3'; import { PublisherBase, TechDocsMetadata } from './types'; @@ -41,13 +42,15 @@ const createMockEntityName = (): EntityName => ({ namespace: 'test-namespace', }); +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; + const getEntityRootDir = (entity: Entity) => { const { kind, metadata: { namespace, name }, } = entity; - const entityRootDir = path.join(namespace as string, kind, name); - return entityRootDir; + + return path.join(rootDir, namespace as string, kind, name); }; const logger = winston.createLogger(); @@ -57,6 +60,7 @@ jest.spyOn(logger, 'error').mockReturnValue(logger); let publisher: PublisherBase; beforeEach(() => { + mockFs.restore(); const mockConfig = new ConfigReader({ techdocs: { requestUrl: 'http://localhost:7000', @@ -78,6 +82,10 @@ beforeEach(() => { describe('AwsS3Publish', () => { describe('publish', () => { + afterEach(() => { + mockFs.restore(); + }); + it('should publish a directory', async () => { const entity = createMockEntity(); const entityRootDir = getEntityRootDir(entity); @@ -98,11 +106,11 @@ describe('AwsS3Publish', () => { directory: entityRootDir, }), ).toBeUndefined(); - mockFs.restore(); }); it('should fail to publish a directory', async () => { const wrongPathToGeneratedDirectory = path.join( + rootDir, 'wrong', 'path', 'to', @@ -126,13 +134,18 @@ describe('AwsS3Publish', () => { entity, directory: wrongPathToGeneratedDirectory, }) - .catch(error => - expect(error).toEqual( - new Error( - `Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, + .catch(error => { + expect(error.message).toEqual( + // Can not do exact error message match due to mockFs adding unexpected characters in the path when throwing the error + // Issue reported https://github.com/tschaub/mock-fs/issues/118 + expect.stringContaining( + `Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory`, ), - ), - ); + ); + expect(error.message).toEqual( + expect.stringContaining(wrongPathToGeneratedDirectory), + ); + }); mockFs.restore(); }); }); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index b0b29e8654..6145ec6365 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -140,9 +140,17 @@ export class AwsS3Publish implements PublisherBase { // Remove the absolute path prefix of the source directory // Path of all files to upload, relative to the root of the source directory // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] - const relativeFilePath = filePath.replace(`${directory}/`, ''); + const relativeFilePath = path.relative(directory, filePath); + + // Convert destination file path to a POSIX path for uploading. + // S3 expects / as path separator and relativeFilePath will contain \\ on Windows. + // https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-keys.html + const relativeFilePathPosix = relativeFilePath + .split(path.sep) + .join(path.posix.sep); + const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - const destination = `${entityRootDir}/${relativeFilePath}`; // S3 Bucket file relative path + const destination = `${entityRootDir}/${relativeFilePathPosix}`; // S3 Bucket file relative path const fileContent = await fs.readFile(filePath, 'utf8');