Merge pull request #5617 from backstage/iameap/spaces-in-doc-objects

This commit is contained in:
Himanshu Mishra
2021-05-10 12:27:03 +02:00
committed by GitHub
12 changed files with 213 additions and 19 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/techdocs-common': patch
---
Fixed a bug that prevented loading static assets from GCS, S3, Azure, and OpenStackSwift whose keys contain spaces or other special characters.
@@ -71,9 +71,9 @@ export class BlockBlobClient {
download() {
const filePath = path.join(rootDir, this.blobName);
const emitter = new EventEmitter();
process.nextTick(() => {
setTimeout(() => {
if (fs.existsSync(filePath)) {
emitter.emit('data', Buffer.from(fs.readFileSync(filePath)));
emitter.emit('data', fs.readFileSync(filePath));
emitter.emit('end');
} else {
emitter.emit(
@@ -81,7 +81,7 @@ export class BlockBlobClient {
new Error(`The file ${filePath} does not exist !`),
);
}
});
}, 0);
return Promise.resolve({
readableStreamBody: emitter,
});
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { EventEmitter } from 'events';
import { Readable } from 'stream';
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
@@ -58,19 +58,21 @@ class GCSFile {
}
createReadStream() {
const emitter = new EventEmitter();
const readable = new Readable();
readable._read = () => {};
process.nextTick(() => {
if (fs.existsSync(this.localFilePath)) {
emitter.emit('data', Buffer.from(fs.readFileSync(this.localFilePath)));
emitter.emit('end');
readable.emit('data', fs.readFileSync(this.localFilePath));
readable.emit('end');
} else {
emitter.emit(
readable.emit(
'error',
new Error(`The file ${this.localFilePath} does not exist !`),
);
}
});
return emitter;
return readable;
}
}
+3 -1
View File
@@ -67,7 +67,9 @@
"@types/mime-types": "^2.1.0",
"@types/mock-fs": "^4.13.0",
"@types/pkgcloud": "^1.7.4",
"@types/recursive-readdir": "^2.2.0"
"@types/recursive-readdir": "^2.2.0",
"@types/supertest": "^2.0.8",
"supertest": "^6.1.3"
},
"jest": {
"roots": [
@@ -21,6 +21,8 @@ import {
ENTITY_DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
@@ -272,4 +274,49 @@ describe('AwsS3Publish', () => {
});
});
});
describe('docsRouter', () => {
let app: express.Express;
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
beforeEach(() => {
app = express().use(publisher.docsRouter());
mockFs.restore();
mockFs({
[entityRootDir]: {
img: {
'with spaces.png': 'found it',
},
'some folder': {
'also with spaces.js': 'found it too',
},
},
});
});
afterEach(() => {
mockFs.restore();
});
it('should pass expected object path to bucket', async () => {
const {
kind,
metadata: { namespace, name },
} = entity;
// Ensures leading slash is trimmed and encoded path is decoded.
const pngResponse = await request(app).get(
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
);
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
'found it',
);
const jsResponse = await request(app).get(
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
});
});
@@ -263,9 +263,9 @@ export class AwsS3Publish implements PublisherBase {
*/
docsRouter(): express.Handler {
return async (req, res) => {
// Trim the leading forward slash
// Decode and trim the leading forward slash
// filePath example - /default/Component/documented-component/index.html
const filePath = req.path.replace(/^\//, '');
const filePath = decodeURI(req.path.replace(/^\//, ''));
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = path.extname(filePath);
@@ -21,6 +21,8 @@ import {
ENTITY_DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
@@ -329,4 +331,49 @@ describe('publishing with valid credentials', () => {
);
});
});
describe('docsRouter', () => {
let app: express.Express;
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
beforeEach(() => {
app = express().use(publisher.docsRouter());
mockFs.restore();
mockFs({
[entityRootDir]: {
img: {
'with spaces.png': 'found it',
},
'some folder': {
'also with spaces.js': 'found it too',
},
},
});
});
afterEach(() => {
mockFs.restore();
});
it('should pass expected object path to bucket', async () => {
const {
kind,
metadata: { namespace, name },
} = entity;
// Ensures leading slash is trimmed and encoded path is decoded.
const pngResponse = await request(app).get(
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
);
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
'found it',
);
const jsResponse = await request(app).get(
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
});
});
@@ -254,9 +254,9 @@ export class AzureBlobStoragePublish implements PublisherBase {
*/
docsRouter(): express.Handler {
return (req, res) => {
// Trim the leading forward slash
// Decode and trim the leading forward slash
// filePath example - /default/Component/documented-component/index.html
const filePath = req.path.replace(/^\//, '');
const filePath = decodeURI(req.path.replace(/^\//, ''));
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = platformPath.extname(filePath);
const responseHeaders = getHeadersForFileExtension(fileExtension);
@@ -21,6 +21,8 @@ import {
EntityName,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
@@ -271,4 +273,47 @@ describe('GoogleGCSPublish', () => {
});
});
});
describe('docsRouter', () => {
let app: express.Express;
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
beforeEach(() => {
app = express().use(publisher.docsRouter());
mockFs.restore();
mockFs({
[entityRootDir]: {
img: {
'with spaces.png': 'found it',
},
'some folder': {
'also with spaces.js': 'found it too',
},
},
});
});
afterEach(() => {
mockFs.restore();
});
it('should pass expected object path to bucket', async () => {
const {
kind,
metadata: { namespace, name },
} = entity;
// Ensures leading slash is trimmed and encoded path is decoded.
const pngResponse = await request(app).get(
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
);
expect(pngResponse.text).toEqual('found it');
const jsResponse = await request(app).get(
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
});
});
@@ -187,9 +187,9 @@ export class GoogleGCSPublish implements PublisherBase {
*/
docsRouter(): express.Handler {
return (req, res) => {
// Trim the leading forward slash
// Decode and trim the leading forward slash
// filePath example - /default/Component/documented-component/index.html
const filePath = req.path.replace(/^\//, '');
const filePath = decodeURI(req.path.replace(/^\//, ''));
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = path.extname(filePath);
@@ -21,6 +21,8 @@ import {
ENTITY_DEFAULT_NAMESPACE,
} from '@backstage/catalog-model';
import { ConfigReader } from '@backstage/config';
import express from 'express';
import request from 'supertest';
import mockFs from 'mock-fs';
import os from 'os';
import path from 'path';
@@ -277,4 +279,49 @@ describe('OpenStackSwiftPublish', () => {
});
});
});
describe('docsRouter', () => {
let app: express.Express;
const entity = createMockEntity();
const entityRootDir = getEntityRootDir(entity);
beforeEach(() => {
app = express().use(publisher.docsRouter());
mockFs.restore();
mockFs({
[entityRootDir]: {
img: {
'with spaces.png': 'found it',
},
'some folder': {
'also with spaces.js': 'found it too',
},
},
});
});
afterEach(() => {
mockFs.restore();
});
it('should pass expected object path to bucket', async () => {
const {
kind,
metadata: { namespace, name },
} = entity;
// Ensures leading slash is trimmed and encoded path is decoded.
const pngResponse = await request(app).get(
`/${namespace}/${kind}/${name}/img/with%20spaces.png`,
);
expect(Buffer.from(pngResponse.body).toString('utf8')).toEqual(
'found it',
);
const jsResponse = await request(app).get(
`/${namespace}/${kind}/${name}/some%20folder/also%20with%20spaces.js`,
);
expect(jsResponse.text).toEqual('found it too');
});
});
});
@@ -221,10 +221,9 @@ export class OpenStackSwiftPublish implements PublisherBase {
*/
docsRouter(): express.Handler {
return async (req, res) => {
// Trim the leading forward slash
// Decode and trim the leading forward slash
// filePath example - /default/Component/documented-component/index.html
const filePath = req.path.replace(/^\//, '');
const filePath = decodeURI(req.path.replace(/^\//, ''));
// Files with different extensions (CSS, HTML) need to be served with different headers
const fileExtension = path.extname(filePath);