Skip to content

Commit

Permalink
[FEATURE] Add option to use hash signatures in cachebuster info file
Browse files Browse the repository at this point in the history
- There are situations where using timestamps may not be reliable
  (ex: CI environments)
- Provide two different signature type to be used: time and hash
- Read configuration parameter from ui5.yaml file, keeping time as default one
  • Loading branch information
dariozilocchi authored and RandomByte committed Apr 25, 2019
1 parent b88037f commit a4e8338
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 4 deletions.
36 changes: 33 additions & 3 deletions lib/tasks/generateCachebusterInfo.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
const resourceFactory = require("@ui5/fs").resourceFactory;
const crypto = require("crypto");

async function signByTime(resource) {
return resource.getStatInfo().mtime.getTime();
}

async function signByHash(resource) {
const hasher = crypto.createHash("sha1");
const buffer = await resource.getBuffer();

hasher.update(buffer.toString("binary"));
return hasher.digest("hex");
}

function getSigner(type) {
type = type || "time";

switch (type) {
case "time":
return signByTime;
case "hash":
return signByHash;

default:
throw new Error(`Invalid signature type: '${type}'. Valid ones are: 'time' or 'hash'`);
}
}

/**
* Task to generate the application cachebuster info file.
Expand All @@ -10,17 +37,20 @@ const resourceFactory = require("@ui5/fs").resourceFactory;
* @param {module:@ui5/fs.AbstractReader} parameters.dependencies Reader or Collection to read dependency files
* @param {Object} parameters.options Options
* @param {string} parameters.options.namespace Namespace of the application
* @param {string} [parameters.options.signatureType='time'] Type of signature to be used ('time' or 'hash')
* @returns {Promise<undefined>} Promise resolving with <code>undefined</code> once data has been written
*/
module.exports = function({workspace, dependencies, options}) {
return workspace.byGlob(`/resources/${options.namespace}/**/*`)
.then(async (resources) => {
const cachebusterInfo = {};
const regex = new RegExp(`^/resources/${options.namespace}/`);
resources.forEach((resource) => {
const signer = getSigner(options.signatureType);

await Promise.all(resources.map(async (resource) => {
const normalizedPath = resource.getPath().replace(regex, "");
cachebusterInfo[normalizedPath] = resource.getStatInfo().mtime.getTime();
});
cachebusterInfo[normalizedPath] = await signer(resource);
}));
const cachebusterInfoResource = resourceFactory.createResource({
path: `/resources/${options.namespace}/sap-ui-cachebuster-info.json`,
string: JSON.stringify(cachebusterInfo, null, 2)
Expand Down
5 changes: 4 additions & 1 deletion lib/types/application/ApplicationBuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,10 @@ class ApplicationBuilder extends AbstractBuilder {
workspace: resourceCollections.workspace,
dependencies: resourceCollections.dependencies,
options: {
namespace: project.metadata.namespace
namespace: project.metadata.namespace,
signatureType: project.builder
&& project.builder.cachebuster
&& project.builder.cachebuster.signatureType,
}
});
});
Expand Down
63 changes: 63 additions & 0 deletions test/lib/tasks/generateCachebusterInfo.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,39 @@ test("integration: Build application.g with manifestBundler", (t) => {
});
});

test("integration: Build application.g with manifestBundler and cachebuster using hashes", (t) => {
const destPath = path.join("test", "tmp", "build", "application.g", "cachebuster_hash");
const expectedPath = path.join("test", "expected", "build", "application.g", "cachebuster");
const excludedTasks = ["generateVersionInfo"];
const includedTasks = ["generateCachebusterInfo"];

return builder.build({
tree: applicationGTreeWithCachebusterHash,
destPath,
excludedTasks,
includedTasks
}).then(() => {
return findFiles(expectedPath);
}).then((expectedFiles) => {
// Check for all directories and files
assert.directoryDeepEqual(destPath, expectedPath);

// Check for all file contents
expectedFiles.forEach((expectedFile) => {
const relativeFile = path.relative(expectedPath, expectedFile);
const destFile = path.join(destPath, relativeFile);
if (expectedFile.endsWith("sap-ui-cachebuster-info.json")) {
const currentContent = JSON.parse(fs.readFileSync(destFile, "utf-8").replace(/(:\s+)("[^"]+")/g, ": \"\""));
const expectedContent = JSON.parse(fs.readFileSync(expectedFile, "utf-8").replace(/(:\s+)(\d+)/g, ": \"\""));
assert.deepEqual(currentContent, expectedContent);
} else {
assert.fileEqual(destFile, expectedFile);
}
});
t.pass();
});
});

const applicationGTree = {
"id": "application.g",
"version": "1.0.0",
Expand All @@ -82,3 +115,33 @@ const applicationGTree = {
}
}
};

const applicationGTreeWithCachebusterHash = {
"id": "application.g",
"version": "1.0.0",
"path": applicationGPath,
"dependencies": [],
"builder": {
"cachebuster": {
"signatureType": "hash"
}
},
"_level": 0,
"specVersion": "0.1",
"type": "application",
"metadata": {
"name": "application.g",
"namespace": "application.g",
"copyright": "Some fancy copyright"
},
"resources": {
"configuration": {
"paths": {
"webapp": "webapp"
}
},
"pathMappings": {
"/": "webapp"
}
}
};

0 comments on commit a4e8338

Please sign in to comment.