Skip to content

Commit c62d7e2

Browse files
feat(cdn): use single fingerprint for all files in a package (#150)
* feat(cdn): use single fingerprint for all files in a package * refactor(cdn): for single fingerprint combine all individual fingerprint ids * test(cdn): verify that the expected files are uploaded to s3 * style(cdn): fix lint issues
1 parent 6bef258 commit c62d7e2

File tree

5 files changed

+107
-15
lines changed

5 files changed

+107
-15
lines changed

lib/plugins/cdn.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const fp = require('fastify-plugin');
2-
const finger = require('fingerprinting');
32
const mime = require('mime-types');
43
const tar = require('tar');
4+
const finger = require('fingerprinting');
55

66
/**
77
* @typedef {import('aws-sdk').S3.PutObjectOutput} AWSPutObjectResponse
@@ -63,9 +63,10 @@ module.exports = fp(
6363
* Unpack tarball and process files and metadata
6464
*
6565
* @param {ReadableStream} stream Tarball readable stream
66+
* @param {boolean} useSingleFingerprint Use single fingerprint id for all files
6667
* @returns {Promise<WrhsAssets>} Warehouse assets
6768
*/
68-
async function tarballToAssets(stream) {
69+
async function tarballToAssets(stream, useSingleFingerprint) {
6970
const files = [];
7071
let metadata = {};
7172

@@ -102,6 +103,20 @@ module.exports = fp(
102103
.on('finish', resolve);
103104
});
104105

106+
if (useSingleFingerprint) {
107+
// make a new ids content combining all the individual ids
108+
const singleIdContent = files.reduce(
109+
(acc, { id }) => `${acc}${id}`,
110+
''
111+
);
112+
const { id: singleId } = finger('singleFingerPrint.txt', {
113+
content: Buffer.from(singleIdContent)
114+
});
115+
files.forEach((file) => {
116+
file.id = singleId;
117+
});
118+
}
119+
105120
return { metadata, files };
106121
}
107122
);

lib/routes/cdn.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ module.exports =
3232
type: 'object',
3333
properties: {
3434
expiration: { type: ['string', 'number'] },
35-
cdn_base_url: { type: 'string' }
35+
cdn_base_url: { type: 'string' },
36+
use_single_fingerprint: { type: 'boolean' }
3637
}
3738
},
3839
response: {
@@ -76,7 +77,7 @@ module.exports =
7677
*/
7778
async (req, res) => {
7879
const {
79-
query: { expiration, cdn_base_url: cdnBaseUrl }
80+
query: { expiration, cdn_base_url: cdnBaseUrl, use_single_fingerprint: useSingleFingerprint }
8081
} = req;
8182
const nowMs = Date.now();
8283
let expTimestamp = null;
@@ -98,7 +99,7 @@ module.exports =
9899
);
99100
}
100101

101-
const wrhsAssets = await fastify.tarballToAssets(req.raw);
102+
const wrhsAssets = await fastify.tarballToAssets(req.raw, useSingleFingerprint);
102103
const { files, metadata: metas } = wrhsAssets;
103104
await fastify.uploadAssetFilesToStorage(files, expTimestamp);
104105

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,8 @@
3333
"eslint": "^8.57.0",
3434
"eslint-config-godaddy": "^7.1.0",
3535
"fastify": "^3.29.5",
36-
"fastify-plugin": "^4.5.1",
3736
"fastify-cli": "^2.3.0",
37+
"fastify-plugin": "^4.5.1",
3838
"nock": "^13.5.4",
3939
"nodemon": "^2.0.22",
4040
"pino-pretty": "^10.0.0",

test/routes/cdn.test.js

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,9 @@ const { build } = require('../helper');
88
test('CDN API', async (t) => {
99
const fastify = build(t);
1010

11-
t.plan(1);
11+
t.plan(2);
1212

1313
t.test('upload assets', async (t) => {
14-
t.plan(3);
15-
1614
const tarball = await fs.readFile(
1715
path.join(__dirname, '..', 'fixtures', 'files', 'my-tarball.tgz')
1816
);
@@ -58,9 +56,87 @@ test('CDN API', async (t) => {
5856
.listObjectsV2({ Bucket: 'warehouse-cdn' })
5957
.promise();
6058
const filenames = files.map(({ Key }) => Key);
61-
t.same(filenames, [
62-
'574d0c0f86b220913f60ee7aae20ec6a/main.css',
63-
'71fbac4eca64da6727d4a9c9cd00e353/main.js'
64-
]);
59+
60+
const expectedFiles = [
61+
'71fbac4eca64da6727d4a9c9cd00e353/main.js',
62+
'574d0c0f86b220913f60ee7aae20ec6a/main.css'
63+
];
64+
65+
expectedFiles.forEach(file => {
66+
t.ok(filenames.includes(file), `${file} should be uploaded to the bucket`);
67+
});
68+
69+
t.end();
70+
});
71+
72+
t.test('upload assets with single fingerprint', async (t) => {
73+
const tarball = await fs.readFile(
74+
path.join(__dirname, '..', 'fixtures', 'files', 'my-tarball.tgz')
75+
);
76+
77+
const res = await fastify.inject({
78+
method: 'POST',
79+
url: '/cdn',
80+
payload: tarball,
81+
query: {
82+
use_single_fingerprint: true
83+
}
84+
});
85+
86+
t.equal(res.statusCode, 201);
87+
88+
const payload = JSON.parse(res.payload);
89+
const payloadFiles = payload.files.map(file => file.url);
90+
91+
// file url is in format https://cdn-example.com/fingerPrintId0/main.js,
92+
// capture the fingerPrintId0 by taking second last element of the url from the first file
93+
const fingerPrintId0 = payloadFiles[0].split('/').splice(-2, 1)[0];
94+
95+
const regex = new RegExp(`https://cdn-example\\.com/${fingerPrintId0}/main\\.(js|css)$`);
96+
t.ok(payloadFiles.every(file => regex.test(file)), 'All file URLs should match the expected pattern keeping the same fingerprint id');
97+
98+
t.same(payload, {
99+
fingerprints: [
100+
'318a308660ba069e74d756cdc854ca52.gz',
101+
'318a308660ba069e74d756cdc854ca52.gz'
102+
],
103+
recommended: [
104+
'318a308660ba069e74d756cdc854ca52/main.js',
105+
'318a308660ba069e74d756cdc854ca52/main.css'
106+
],
107+
files: [
108+
{
109+
url: 'https://cdn-example.com/318a308660ba069e74d756cdc854ca52/main.js',
110+
metadata: {
111+
css: false,
112+
js: true,
113+
foo: 'bar'
114+
}
115+
},
116+
{
117+
url: 'https://cdn-example.com/318a308660ba069e74d756cdc854ca52/main.css',
118+
metadata: {
119+
css: true,
120+
js: false,
121+
beep: 'boop'
122+
}
123+
}
124+
]
125+
});
126+
127+
const { Contents: files } = await fastify.s3
128+
.listObjectsV2({ Bucket: 'warehouse-cdn' })
129+
.promise();
130+
const filenames = files.map(({ Key }) => Key);
131+
const expectedFiles = [
132+
'318a308660ba069e74d756cdc854ca52/main.js',
133+
'318a308660ba069e74d756cdc854ca52/main.css'
134+
];
135+
136+
expectedFiles.forEach(file => {
137+
t.ok(filenames.includes(file), `${file} should be uploaded to the bucket`);
138+
});
139+
140+
t.end();
65141
});
66142
});

0 commit comments

Comments
 (0)