TechDocs/GCS: Fix publisher to work with Windows file path

This commit is contained in:
Himanshu Mishra
2021-02-18 20:55:35 +01:00
parent 1966cc1802
commit 4b3500021c
4 changed files with 82 additions and 27 deletions
@@ -13,10 +13,31 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import fs from 'fs-extra';
import path from 'path';
type storageOptions = {
keyFilename?: string;
};
/**
* @param sourceFile contains either / or \ as file separator depending upon OS.
*/
const checkFileExists = async (sourceFile: string): Promise<boolean> => {
// sourceFile will always have / as file separator irrespective of OS since S3 expects /.
// Normalize sourceFile to OS specific path before checking if file exists.
const filePath = sourceFile.split(path.posix.sep).join(path.sep);
try {
await fs.access(filePath, fs.constants.F_OK);
return true;
} catch (err) {
console.log("File doesn't exist");
console.log(filePath);
return false;
}
};
class Bucket {
private readonly bucketName;
@@ -31,8 +52,12 @@ class Bucket {
}
upload(source: string, { destination }) {
return new Promise(resolve => {
resolve({ source, destination });
return new Promise(async (resolve, reject) => {
if (await checkFileExists(source)) {
resolve({ source, destination });
} else {
reject(`Source file ${source} does not exist.`);
}
});
}
}
@@ -22,7 +22,7 @@ import path from 'path';
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
/**
* @param Key Contains either / or \ as file separator depending upon OS.
* @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 /.
@@ -13,18 +13,22 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import mockFs from 'mock-fs';
import * as winston from 'winston';
import { getVoidLogger } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
import { GoogleGCSPublish } from './googleStorage';
import { PublisherBase } from './types';
const createMockEntity = (annotations = {}) => {
const createMockEntity = (annotations = {}): Entity => {
return {
apiVersion: 'version',
kind: 'TestKind',
metadata: {
name: 'test-component-name',
namespace: 'test-namespace',
annotations: {
...annotations,
},
@@ -32,12 +36,24 @@ const createMockEntity = (annotations = {}) => {
};
};
const logger = winston.createLogger();
const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir';
const getEntityRootDir = (entity: Entity) => {
const {
kind,
metadata: { namespace, name },
} = entity;
return path.join(rootDir, namespace as string, kind, name);
};
const logger = getVoidLogger();
jest.spyOn(logger, 'info').mockReturnValue(logger);
let publisher: PublisherBase;
beforeEach(async () => {
mockFs.restore();
const mockConfig = new ConfigReader({
techdocs: {
requestUrl: 'http://localhost:7000',
@@ -55,9 +71,16 @@ beforeEach(async () => {
});
describe('GoogleGCSPublish', () => {
afterEach(() => {
mockFs.restore();
});
it('should publish a directory', async () => {
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
mockFs({
'/path/to/generatedDirectory': {
[entityRootDir]: {
'index.html': '',
'404.html': '',
assets: {
@@ -66,11 +89,10 @@ describe('GoogleGCSPublish', () => {
},
});
const entity = createMockEntity();
expect(
await publisher.publish({
entity,
directory: '/path/to/generatedDirectory',
directory: entityRootDir,
}),
).toBeUndefined();
mockFs.restore();
@@ -13,20 +13,20 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import path from 'path';
import express from 'express';
import {
Storage,
UploadResponse,
FileExistsResponse,
} from '@google-cloud/storage';
import { Logger } from 'winston';
import { Entity, EntityName } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers';
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
import {
FileExistsResponse,
Storage,
UploadResponse,
} from '@google-cloud/storage';
import express from 'express';
import JSON5 from 'json5';
import createLimiter from 'p-limit';
import path from 'path';
import { Logger } from 'winston';
import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers';
import { PublisherBase, PublishRequest, TechDocsMetadata } from './types';
export class GoogleGCSPublish implements PublisherBase {
static async fromConfig(
@@ -105,19 +105,27 @@ export class GoogleGCSPublish implements PublisherBase {
const limiter = createLimiter(10);
const uploadPromises: Array<Promise<UploadResponse>> = [];
allFilesToUpload.forEach(filePath => {
allFilesToUpload.forEach(sourceFilePath => {
// 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, sourceFilePath);
// Convert destination file path to a POSIX path for uploading.
// GCS expects / as path separator and relativeFilePath will contain \\ on Windows.
// https://cloud.google.com/storage/docs/gsutil/addlhelp/HowSubdirectoriesWork
const relativeFilePathPosix = relativeFilePath
.split(path.sep)
.join(path.posix.sep);
const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`;
const destination = `${entityRootDir}/${relativeFilePath}`; // GCS Bucket file relative path
const destination = `${entityRootDir}/${relativeFilePathPosix}`; // GCS Bucket file relative path
// Rate limit the concurrent execution of file uploads to batches of 10 (per publish)
const uploadFile = limiter(() =>
this.storageClient
.bucket(this.bucketName)
.upload(filePath, { destination }),
this.storageClient.bucket(this.bucketName).upload(sourceFilePath, {
destination,
}),
);
uploadPromises.push(uploadFile);
});
@@ -130,7 +138,7 @@ export class GoogleGCSPublish implements PublisherBase {
resolve(undefined);
})
.catch((err: Error) => {
const errorMessage = `Unable to upload file(s) to Google Cloud Storage. Error ${err.message}`;
const errorMessage = `Unable to upload file(s) to Google Cloud Storage. Error ${err}`;
this.logger.error(errorMessage);
reject(errorMessage);
});