diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 4ffc3edae6..b53bd0ca0a 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -25,7 +25,11 @@ import createLimiter from 'p-limit'; import path from 'path'; import { Readable } from 'stream'; import { Logger } from 'winston'; -import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; +import { + getFileTreeRecursively, + getHeadersForFileExtension, + lowerCaseEntityTripletInStoragePath, +} from './helpers'; import { PublisherBase, PublishRequest, @@ -332,17 +336,10 @@ export class AwsS3Publish implements PublisherBase { await Promise.all( allObjects.map(f => limiter(async file => { - const [namespace, kind, name, ...parts] = file.split('/'); - const lowerNamespace = namespace.toLowerCase(); - const lowerKind = kind.toLowerCase(); - const lowerName = name.toLowerCase(); + const newPath = lowerCaseEntityTripletInStoragePath(file); // If all parts are already lowercase, ignore. - if ( - namespace === lowerNamespace && - kind === lowerKind && - name === lowerName - ) { + if (file === newPath) { return; } @@ -352,7 +349,7 @@ export class AwsS3Publish implements PublisherBase { .copyObject({ Bucket: this.bucketName, CopySource: [this.bucketName, file].join('/'), - Key: [lowerNamespace, lowerKind, lowerName, ...parts].join('/'), + Key: newPath, }) .promise(); diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index ddc70d5b46..e94589b8df 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -25,7 +25,11 @@ import JSON5 from 'json5'; import limiterFactory from 'p-limit'; import { default as path, default as platformPath } from 'path'; import { Logger } from 'winston'; -import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; +import { + getFileTreeRecursively, + getHeadersForFileExtension, + lowerCaseEntityTripletInStoragePath, +} from './helpers'; import { PublisherBase, PublishRequest, @@ -290,14 +294,6 @@ export class AzureBlobStoragePublish implements PublisherBase { .exists(); } - protected toLowerCase(originalName: string): string { - const [namespace, kind, name, ...parts] = originalName.split('/'); - const lowerNamespace = namespace.toLowerCase(); - const lowerKind = kind.toLowerCase(); - const lowerName = name.toLowerCase(); - return [lowerNamespace, lowerKind, lowerName, ...parts].join('/'); - } - protected async renameBlob( originalName: string, newName: string, @@ -314,16 +310,16 @@ export class AzureBlobStoragePublish implements PublisherBase { } protected async renameBlobToLowerCase( - originalName: string, + originalPath: string, removeOriginal: boolean, ) { - const newName = this.toLowerCase(originalName); - if (originalName === newName) return; + const newPath = lowerCaseEntityTripletInStoragePath(originalPath); + if (originalPath === newPath) return; try { - this.logger.debug(`Migrating ${originalName}`); - await this.renameBlob(originalName, newName, removeOriginal); + this.logger.debug(`Migrating ${originalPath}`); + await this.renameBlob(originalPath, newPath, removeOriginal); } catch (e) { - this.logger.warn(`Unable to migrate ${originalName}: ${e.message}`); + this.logger.warn(`Unable to migrate ${originalPath}: ${e.message}`); } } diff --git a/packages/techdocs-common/src/stages/publish/helpers.test.ts b/packages/techdocs-common/src/stages/publish/helpers.test.ts index cfb9c7930d..a650b59a54 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.test.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.test.ts @@ -16,7 +16,11 @@ import mockFs from 'mock-fs'; import * as os from 'os'; import * as path from 'path'; -import { getFileTreeRecursively, getHeadersForFileExtension } from './helpers'; +import { + getFileTreeRecursively, + getHeadersForFileExtension, + lowerCaseEntityTripletInStoragePath, +} from './helpers'; describe('getHeadersForFileExtension', () => { const correctMapOfExtensions = [ @@ -73,3 +77,17 @@ describe('getFileTreeRecursively', () => { expect(fileList).toContain(path.resolve(root, 'subDirA/file2')); }); }); + +describe('lowerCaseEntityTripletInStoragePath', () => { + it('returns lower-cased entity triplet path', () => { + const originalPath = 'default/Component/backstage/index.html'; + const actualPath = lowerCaseEntityTripletInStoragePath(originalPath); + expect(actualPath).toBe('default/component/backstage/index.html'); + }); + + it('does not lowercase beyond the triplet', () => { + const originalPath = 'default/Component/backstage/assets/IMAGE.png'; + const actualPath = lowerCaseEntityTripletInStoragePath(originalPath); + expect(actualPath).toBe('default/component/backstage/assets/IMAGE.png'); + }); +}); diff --git a/packages/techdocs-common/src/stages/publish/helpers.ts b/packages/techdocs-common/src/stages/publish/helpers.ts index eac5ba05c8..d26a4d71f1 100644 --- a/packages/techdocs-common/src/stages/publish/helpers.ts +++ b/packages/techdocs-common/src/stages/publish/helpers.ts @@ -82,3 +82,24 @@ export const getFileTreeRecursively = async ( }); return fileList; }; + +/** + * Returns the version of an object's storage path where the first three parts + * of the path (the entity triplet of namespace, kind, and name) are + * lower-cased. + * + * Path must not include a starting slash. + * + * @example + * lowerCaseEntityTripletInStoragePath('default/Component/backstage') + * // return default/component/backstage + */ +export const lowerCaseEntityTripletInStoragePath = ( + originalPath: string, +): string => { + const [namespace, kind, name, ...parts] = originalPath.split('/'); + const lowerNamespace = namespace.toLowerCase(); + const lowerKind = kind.toLowerCase(); + const lowerName = name.toLowerCase(); + return [lowerNamespace, lowerKind, lowerName, ...parts].join('/'); +}; diff --git a/packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts b/packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts index 6bac7ace4a..a2b5b8d719 100644 --- a/packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts +++ b/packages/techdocs-common/src/stages/publish/migrations/GoogleMigration.ts @@ -17,6 +17,7 @@ import { File } from '@google-cloud/storage'; import { Writable } from 'stream'; import { Logger } from 'winston'; +import { lowerCaseEntityTripletInStoragePath } from '../helpers'; /** * Writable stream to handle object copy/move operations. This implementation @@ -37,17 +38,10 @@ export class MigrateWriteStream extends Writable { _write(file: File, _encoding: BufferEncoding, next: Function) { let shouldCallNext = true; - const [namespace, kind, name, ...parts] = file.name.split('/'); - const lowerNamespace = namespace.toLowerCase(); - const lowerKind = kind.toLowerCase(); - const lowerName = name.toLowerCase(); + const newFile = lowerCaseEntityTripletInStoragePath(file.name); // If all parts are already lowercase, ignore. - if ( - namespace === lowerNamespace && - kind === lowerKind && - name === lowerName - ) { + if (newFile === file.name) { next(); return; } @@ -60,7 +54,6 @@ export class MigrateWriteStream extends Writable { } // Otherwise, copy or move the file. - const newFile = [lowerNamespace, lowerKind, lowerName, ...parts].join('/'); // todo: Use file.move instead of file.copy when removeOriginal is true. const migrate = this.removeOriginal ? file.copy.bind(file)