Skip to content

Commit e5fa80a

Browse files
committed
beginning abstract cloudinary and util functions with tests
1 parent 7dae049 commit e5fa80a

File tree

9 files changed

+2441
-139
lines changed

9 files changed

+2441
-139
lines changed

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"description": "",
55
"main": "src/index.js",
66
"scripts": {
7-
"test": "echo \"Error: no test specified\" && exit 1"
7+
"test": "jest"
88
},
99
"author": {
1010
"name": "Colby Fayock",
@@ -17,5 +17,8 @@
1717
"glob": "^7.2.0",
1818
"jsdom": "^18.1.1",
1919
"node-fetch": "2"
20+
},
21+
"devDependencies": {
22+
"jest": "^27.4.3"
2023
}
2124
}

src/index.js

Lines changed: 24 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
const fs = require('fs').promises;
2-
const path = require('path');
3-
const crypto = require('crypto');
42
const glob = require('glob');
5-
const fetch = require('node-fetch');
63
const { JSDOM } = require('jsdom');
7-
const cloudinary = require('cloudinary').v2;
4+
5+
const { getCloudinary, getCloudinaryUrl } = require('./lib/cloudinary');
86

97
/**
108
* TODO
@@ -30,6 +28,8 @@ module.exports = {
3028
throw new Error('Cloudinary Cloud Name required. Please use environment variable CLOUDINARY_CLOUD_NAME');
3129
}
3230

31+
const cloudinary = getCloudinary();
32+
3333
cloudinary.config({
3434
cloud_name: cloudName,
3535
api_key: apiKey,
@@ -52,112 +52,31 @@ module.exports = {
5252

5353
for ( const $img of images ) {
5454
let imgSrc = $img.getAttribute('src');
55-
let cloudinarySrc;
56-
57-
if ( deliveryType === 'fetch' ) {
58-
// fetch allows us to pass in a remote URL to the Cloudinary API
59-
// which it will cache and serve from the CDN, but not store
60-
61-
imgSrc = determineRemoteUrl(imgSrc);
62-
63-
cloudinarySrc = cloudinary.url(imgSrc, {
64-
type: deliveryType,
65-
secure: true,
66-
transformation: [
67-
{
68-
fetch_format: 'auto',
69-
quality: 'auto'
70-
}
71-
]
72-
});
73-
} else if ( deliveryType === 'upload' ) {
74-
// upload will actually store the image in the Cloudinary account
75-
// and subsequently serve that stored image
76-
77-
// If our image is locally sourced, we need to obtain the full
78-
// local relative path so that we can tell Cloudinary where
79-
// to upload from
80-
81-
const { name: imgName } = path.parse(imgSrc);
82-
let hash = crypto.createHash('md5');
83-
84-
if ( !isRemoteUrl(imgSrc) ) {
85-
imgSrc = path.join(PUBLISH_DIR, imgSrc);
86-
hash.update(imgSrc);
87-
} else {
88-
const response = await fetch(imgSrc);
89-
const buffer = await response.buffer();
90-
hash.update(buffer);
91-
}
92-
93-
hash = hash.digest('hex');
9455

95-
const id = `${imgName}-${hash}`;
96-
97-
const uploadOptions = {
56+
try {
57+
const cloudinarySrc = await getCloudinaryUrl({
58+
apiKey,
59+
apiSecret,
60+
deliveryType,
9861
folder,
99-
public_id: id
100-
}
101-
102-
let results;
103-
104-
if ( apiKey && apiSecret ) {
105-
// We need an API Key and Secret to use signed uploading
106-
107-
try {
108-
results = await cloudinary.uploader.upload(imgSrc, {
109-
...uploadOptions,
110-
overwrite: false
111-
});
112-
} catch(e) {
113-
const { error } = e;
114-
errors.push({
115-
imgSrc,
116-
message: e.message || error.message
117-
});
118-
continue;
119-
}
120-
} else if ( uploadPreset ) {
121-
// If we want to avoid signing our uploads, we don't need our API Key and Secret,
122-
// however, we need to provide an uploadPreset
123-
124-
try {
125-
results = await cloudinary.uploader.unsigned_upload(imgSrc, uploadPreset, {
126-
...uploadOptions
127-
// Unsigned uploads default to overwrite: false
128-
});
129-
} catch(e) {
130-
const { error } = e;
131-
errors.push({
132-
imgSrc,
133-
message: e.message || error.message
134-
});
135-
continue;
136-
}
137-
} else {
138-
throw new Error(`To use deliveryType ${deliveryType}, please use an uploadPreset for unsigned requests or an API Key and Secret for signed requests.`);
139-
}
140-
141-
// Finally use the stored public ID to grab the image URL
142-
143-
const { public_id } = results;
144-
145-
cloudinarySrc = cloudinary.url(public_id, {
146-
secure: true,
147-
transformation: [
148-
{
149-
fetch_format: 'auto',
150-
quality: 'auto'
151-
}
152-
]
62+
path: imgSrc,
63+
publishDir: PUBLISH_DIR,
64+
uploadPreset,
15365
});
154-
66+
67+
$img.setAttribute('src', cloudinarySrc)
68+
} catch(e) {
69+
const { error } = e;
70+
errors.push({
71+
imgSrc,
72+
message: e.message || error.message
73+
});
74+
continue;
15575
}
156-
157-
$img.setAttribute('src', cloudinarySrc)
158-
159-
await fs.writeFile(page, dom.serialize());
76+
16077
}
78+
79+
await fs.writeFile(page, dom.serialize());
16180
}
16281

16382
if ( errors.length > 0) {
@@ -168,30 +87,4 @@ module.exports = {
16887
}
16988
}
17089

171-
}
172-
173-
/**
174-
* isRemoteUrl
175-
*/
176-
177-
function isRemoteUrl(path) {
178-
return path.startsWith('http');
179-
}
180-
181-
/**
182-
* determineRemoteUrl
183-
*/
184-
185-
function determineRemoteUrl(path) {
186-
if ( isRemoteUrl(path) ) return path;
187-
188-
let url = path;
189-
190-
if ( !path.startsWith('/') ) {
191-
url = `/${url}`;
192-
}
193-
194-
url = `${process.env.DEPLOY_PRIME_URL}${url}`;
195-
196-
return url;
19790
}

src/lib/cloudinary.js

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
const crypto = require('crypto');
2+
const path = require('path');
3+
const fetch = require('node-fetch');
4+
const cloudinary = require('cloudinary').v2;
5+
6+
const { isRemoteUrl, determineRemoteUrl } = require('./util');
7+
8+
/**
9+
* getCloudinary
10+
*/
11+
12+
function getCloudinary() {
13+
return cloudinary;
14+
}
15+
16+
module.exports.getCloudinary = getCloudinary;
17+
18+
/**
19+
* getCloudinaryUrl
20+
*/
21+
22+
async function getCloudinaryUrl(options = {}) {
23+
const {
24+
apiKey,
25+
apiSecret,
26+
deliveryType,
27+
folder,
28+
path: filePath,
29+
publishDir,
30+
uploadPreset,
31+
} = options;
32+
33+
let fileLocation;
34+
35+
if ( deliveryType === 'fetch' ) {
36+
// fetch allows us to pass in a remote URL to the Cloudinary API
37+
// which it will cache and serve from the CDN, but not store
38+
39+
fileLocation = determineRemoteUrl(filePath);
40+
} else if ( deliveryType === 'upload' ) {
41+
// upload will actually store the image in the Cloudinary account
42+
// and subsequently serve that stored image
43+
44+
// If our image is locally sourced, we need to obtain the full
45+
// local relative path so that we can tell Cloudinary where
46+
// to upload from
47+
48+
let fullPath = filePath;
49+
50+
if ( !isRemoteUrl(fullPath) ) {
51+
fullPath = path.join(publishDir, fullPath);
52+
}
53+
54+
const id = await createPublicId({
55+
path: fullPath
56+
});
57+
58+
const uploadOptions = {
59+
folder,
60+
public_id: id
61+
}
62+
63+
let results;
64+
65+
if ( apiKey && apiSecret ) {
66+
// We need an API Key and Secret to use signed uploading
67+
68+
results = await cloudinary.uploader.upload(fullPath, {
69+
...uploadOptions,
70+
overwrite: false
71+
});
72+
} else if ( uploadPreset ) {
73+
// If we want to avoid signing our uploads, we don't need our API Key and Secret,
74+
// however, we need to provide an uploadPreset
75+
76+
results = await cloudinary.uploader.unsigned_upload(fullPath, uploadPreset, {
77+
...uploadOptions
78+
// Unsigned uploads default to overwrite: false
79+
});
80+
} else {
81+
throw new Error(`To use deliveryType ${deliveryType}, please use an uploadPreset for unsigned requests or an API Key and Secret for signed requests.`);
82+
}
83+
84+
// Finally use the stored public ID to grab the image URL
85+
86+
const { public_id } = results;
87+
fileLocation = public_id;
88+
}
89+
90+
const cloudinaryUrl = cloudinary.url(fileLocation, {
91+
type: deliveryType,
92+
secure: true,
93+
transformation: [
94+
{
95+
fetch_format: 'auto',
96+
quality: 'auto'
97+
}
98+
]
99+
});
100+
101+
return cloudinaryUrl;
102+
}
103+
104+
module.exports.getCloudinaryUrl = getCloudinaryUrl;
105+
106+
/**
107+
* createPublicId
108+
*/
109+
110+
async function createPublicId({ path: filePath } = {}) {
111+
let hash = crypto.createHash('md5');
112+
113+
const { name: imgName } = path.parse(filePath);
114+
115+
if ( !isRemoteUrl(filePath) ) {
116+
hash.update(filePath);
117+
} else {
118+
const response = await fetch(filePath);
119+
const buffer = await response.buffer();
120+
hash.update(buffer);
121+
}
122+
123+
hash = hash.digest('hex');
124+
125+
return `${imgName}-${hash}`
126+
}
127+
128+
module.exports.createPublicId = createPublicId;

src/lib/util.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
/**
2+
* isRemoteUrl
3+
*/
4+
5+
function isRemoteUrl(url) {
6+
return url.startsWith('http');
7+
}
8+
9+
module.exports.isRemoteUrl = isRemoteUrl;
10+
11+
/**
12+
* determineRemoteUrl
13+
*/
14+
15+
function determineRemoteUrl(url) {
16+
if ( isRemoteUrl(url) ) return url;
17+
18+
if ( !url.startsWith('/') ) {
19+
url = `/${url}`;
20+
}
21+
22+
url = `${process.env.DEPLOY_PRIME_URL}${url}`;
23+
24+
return url;
25+
}
26+
27+
module.exports.determineRemoteUrl = determineRemoteUrl;
48.8 KB
Loading
37.3 KB
Loading

tests/lib/cloudinary.test.js

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
const { createPublicId } = require('../../src/lib/cloudinary');
2+
3+
describe('lib/util', () => {
4+
5+
describe('createPublicId', () => {
6+
7+
test('should create a public ID from a remote URL', async () => {
8+
const mikeId = await createPublicId({ path: 'https://i.imgur.com/e6XK75j.png' });
9+
expect(mikeId).toEqual('e6XK75j-58e290136642a9c711afa6410b07848d');
10+
11+
const lucasId = await createPublicId({ path: 'https://i.imgur.com/vtYmp1x.png' });
12+
expect(lucasId).toEqual('vtYmp1x-ae71a79c9c36b8d5dba872c3b274a444');
13+
});
14+
15+
test('should create a public ID from a local image', async () => {
16+
const dustinId = await createPublicId({ path: '../images/stranger-things-dustin.jpeg' });
17+
expect(dustinId).toEqual('stranger-things-dustin-9a2a7b1501695c50ad85c329f79fb184');
18+
19+
const elevenId = await createPublicId({ path: '../images/stranger-things-eleven.jpeg' });
20+
expect(elevenId).toEqual('stranger-things-eleven-c5486e412115dbeba03315959c3a6d20');
21+
});
22+
23+
});
24+
25+
});

0 commit comments

Comments
 (0)