TechDocs: AWS Publisher - fix upload path to be OS agnostic

This commit is contained in:
Himanshu Mishra
2021-02-18 14:11:52 +01:00
parent 35952103d6
commit 1966cc1802
3 changed files with 68 additions and 31 deletions
+35 -19
View File
@@ -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<boolean> => {
// 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('');
}
@@ -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();
});
});
@@ -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');