Skip to content

Commit

Permalink
feat(cf-images): creating cloudflare images adapter (#7)
Browse files Browse the repository at this point in the history
#6

* feat: creating cloudflare adapter

* feat: cloudflare integration

* feat: cloudflare integration

* feat: cloudflare integration

* feat: cloudflare integration

* fix: ecom review #1

* fix: ecom review #1

* fix: ecom review #3

* fix: ecom review #4

* fix: ecom review #4

* fix: ecom review #5

* fix: ecom review #6

* fix: ecom review #7

* fix: ecom review #8
  • Loading branch information
Mazurco066 authored Feb 1, 2022
1 parent 534c406 commit 6fe1b0d
Show file tree
Hide file tree
Showing 8 changed files with 1,043 additions and 128 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ typings/

# dotenv environment variables file
.env
config.json
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Edit `config.json` placing correct values for your environment,
after that, start app with node:

```bash
node ./main.js
node ./main.js
```

# Web server
Expand Down
231 changes: 104 additions & 127 deletions bin/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const logger = require('./../lib/Logger')
const auth = require('./../lib/Auth')
// AWS SDK API abstraction
const Aws = require('./../lib/Aws')
// Cloudinary API abstraction
const Cloudinary = require('./../lib/Cloudinary')
// Cloudflare API abstraxtion
const CloudFlare = require('./../lib/Cloudflare')
// download image from Kraken temporary CDN
const download = require('./../lib/Download')

Expand Down Expand Up @@ -38,17 +38,14 @@ fs.readFile(path.join(__dirname, '../config/config.json'), 'utf8', (err, data) =
// hostname,
baseUri,
doSpace,
cloudinaryAuth,
cloudflareAuth,
cdnHost,
pictureSizes
} = JSON.parse(data)

const pictureOptims = (pictureSizes || [700, 350]).reduce((optims, size, i) => {
const label = i === 0 ? 'big' : i === 1 ? 'normal' : 'small'
optims.push(
{ size, label, webp: false },
{ size, label, webp: true }
)
optims.push({ size, label, webp: true })
return optims
}, [])

Expand Down Expand Up @@ -99,8 +96,8 @@ fs.readFile(path.join(__dirname, '../config/config.json'), 'utf8', (err, data) =
return run(spaces[0])
}

// setup Cloudinary client
const cloudinary = Cloudinary(cloudinaryAuth)
// setup Cloudflare client
const cloudflare = CloudFlare(cloudflareAuth)

const sendError = (res, status, code, devMsg, usrMsg) => {
if (!devMsg) {
Expand Down Expand Up @@ -268,86 +265,64 @@ fs.readFile(path.join(__dirname, '../config/config.json'), 'utf8', (err, data) =
Key: `${storeId}/${key}`,
Body: req.file.buffer
})
.then(() => {
logger.log(`${storeId} ${key} Uploaded to ${bucket}`)
// zoom uploaded
const mountUri = (key, baseUrl = cdnHost || host) => `https://${baseUrl}/${storeId}/${key}`
const uri = mountUri(key)
const picture = {
zoom: { url: uri }
}
const pictureBytes = {}
// resize/optimize image
let i = -1
let transformedImageBody = null

const respond = () => {
logger.log(`${storeId} ${key} ${bucket} All optimizations done`)
res.json({
bucket,
key,
// return complete object URL
uri,
picture
})
}
// S3 Response
.then(() => {
logger.log(`${storeId} ${key} Uploaded to ${bucket}`)
// zoom uploaded
const mountUri = (key, baseUrl = cdnHost || host) => `https://${baseUrl}/${storeId}/${key}`
const uri = mountUri(key)
const picture = { zoom: { url: uri } }
const pictureBytes = {}
// resize/optimize image
let i = -1
let transformedImageBody = null

const respond = () => {
logger.log(`${storeId} ${key} ${bucket} All optimizations done`)
res.json({ bucket, key, uri, picture })
}

const callback = err => {
if (!err) {
// next image size
i++
if (i < pictureOptims.length) {
let newKey
const { label, size, webp } = pictureOptims[i]
newKey = `imgs/${label}/${key}`

const imageBuffer = i === 0 ? req.file.buffer : transformedImageBody
const imageBase64 = imageBuffer
? `data:${mimetype};base64,${imageBuffer.toString('base64')}`
: null
// free memory
transformedImageBody = req.file = null

setTimeout(() => {
// image resize/optimization with Cloudinary
let fixSize, originUrl
if (picture[label] && webp) {
fixSize = false
originUrl = picture[label].url
} else {
fixSize = true
originUrl = uri
}

const transformImg = (isRetry = false) => {
cloudinary(imageBase64 || originUrl, fixSize && size, webp, (err, data) => {
const callback = err => {
if (!err) {
// next image size
i++
if (i < pictureOptims.length) {
let newKey
const { label, size, webp } = pictureOptims[i]
newKey = `imgs/${label}/${key}`

const imageBuffer = i === 0 ? req.file.buffer : transformedImageBody
const imageBase64 = imageBuffer
? `data:${mimetype};base64,${imageBuffer.toString('base64')}`
: null
// free memory
transformedImageBody = req.file = null

setTimeout(() => {

// Retrieve url
let originUrl
if (picture[label] && webp) {
originUrl = picture[label].url
} else {
originUrl = uri
}

// Transform image updated to cloudflare
const transformImg = (isRetry = false) => {

cloudflare(imageBase64 || originUrl, label === 'small' ? 'w90' : label, (err, data) => {
if (!err && data) {
const { id, format, url, bytes, imageBody } = data

const { id, url, imageBody } = data
return new Promise(resolve => {
let contentType
if (webp) {
// fix filepath extension and content type header
if (format) {
if (!newKey.endsWith(format)) {
// converted to best optim format
newKey += `.${format}`
}
contentType = format === 'jpg' ? 'image/jpeg' : `image/${format}`
} else {
// converted to WebP
newKey += '.webp'
contentType = 'image/webp'
}
} else {
contentType = mimetype
}

// Cloudinary keeps image as webp, so we using webp as default
const contentType = 'image/webp'
const fileFormat = 'webp'
if (imageBody || id) {
const s3Options = {
...baseS3Options,
ContentType: contentType,
Key: `${storeId}/${newKey}`
Key: `${storeId}/${newKey}.${fileFormat}`
}
if (imageBody) {
transformedImageBody = imageBody
Expand All @@ -364,16 +339,14 @@ fs.readFile(path.join(__dirname, '../config/config.json'), 'utf8', (err, data) =
return resolve(mountUri(newKey))
}
resolve(url)

}).then(url => {
if (url && (!picture[label] || pictureBytes[label] > bytes)) {
picture[label] = { url, size }
pictureBytes[label] = bytes
}
callback()
})

.then(url => {
if (url && (!picture[label] || pictureBytes[label] > bytes)) {
// add to response pictures
picture[label] = { url, size }
pictureBytes[label] = bytes
}
callback()
})
}

if (
Expand All @@ -384,52 +357,56 @@ fs.readFile(path.join(__dirname, '../config/config.json'), 'utf8', (err, data) =
if (!isRetry) {
return setTimeout(() => transformImg(true), 1000)
} else {
// return image without all transformations
return respond()
}
}
callback(err)
})
}
transformImg()
}, imageBase64 ? 50 : 300)
} else {
setTimeout(() => {
// all done
respond()
}, 50)
}
} else if (uri && typeof err.message === 'string' && err.message.indexOf('cloud_name') > -1) {
// image uploaded but not transformed
respond()
logger.error(err)

}

// Transofrm image
transformImg()

}, imageBase64 ? 50 : 300)
} else {
// respond with error
const usrMsg = {
en_us: 'Error while handling image, the file may be protected or corrupted',
pt_br: 'Erro ao manipular a imagem, o arquivo pode estar protegido ou corrompido'
}
sendError(res, 415, uri, err.message, usrMsg)
setTimeout(() => {
// all done
respond()
}, 50)
}
}

switch (mimetype) {
case 'image/jpeg':
case 'image/png':
callback()
break
default:
respond()
} else if (uri && typeof err.message === 'string' && err.message.indexOf('cloud_name') > -1) {
// image uploaded but not transformed
respond()
logger.error(err)
} else {
// respond with error
const usrMsg = {
en_us: 'Error while handling image, the file may be protected or corrupted',
pt_br: 'Erro ao manipular a imagem, o arquivo pode estar protegido ou corrompido'
}
sendError(res, 415, uri, err.message, usrMsg)
}
})
}

.catch(err => {
const usrMsg = {
en_us: 'This file cannot be uploaded to CDN',
pt_br: 'O arquivo não pôde ser carregado para o CDN'
}
sendError(res, 400, 3002, err.message, usrMsg)
})
switch (mimetype) {
case 'image/jpeg':
case 'image/png':
callback()
break
default:
respond()
}
})
// CDN Upload error
.catch((err) => {
const usrMsg = {
en_us: 'This file cannot be uploaded to CDN',
pt_br: 'O arquivo não pôde ser carregado para o CDN'
}
sendError(res, 400, 3002, err.message, usrMsg)
})
}
})
})
Expand Down
4 changes: 4 additions & 0 deletions config/config-sample.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,9 @@
"apiKey": "abcdefgh",
"apiSecret": "1234567890"
},
"cloudflareAuth": {
"apiKey": "your_key",
"accountId": "your_account"
},
"cdnHost": "ecoms1.com"
}
65 changes: 65 additions & 0 deletions lib/Cloudflare.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
'use strict'

// Dependencies
const axios = require('axios').default
const FormData = require('form-data')
const download = require('./RequestDownload')

// Cloudflare module
module.exports = (auth) => {

// Setup axios client with api key
const cloudflareClient = axios.create({
baseURL: `https://api.cloudflare.com/client/v4/accounts/${auth.accountId}`,
headers: { Authorization: `Bearer ${auth.apiKey}` }
})

// Function to Compress image
return function (url, size = 'normal', __callback = () => {}) {

// API Payload
let options = new FormData()
options.append('file', url)

// Force timeout with 20s
let callbackSent = false
const callback = (err, data) => {
if (!callbackSent) {
callbackSent = true
__callback(err, data)
}
if (timer) {
clearTimeout(timer)
}
}

// Verify if responded in 20s
const timer = setTimeout(() => {
callback(new Error('Cloudflare optimization timed out'))
logger.log(`Cloudflare timed out`)
}, 20000)

// Upload image to cloudflare
cloudflareClient({
method: 'POST',
url: '/images/v1',
headers: { ...options.getHeaders() },
data: options
}).then((response) => {
const id = response.data.result.id
const variants = response.data.result.variants
const url = variants.find(v => v.endsWith(`/${size}`)) || variants[0]
if (url) {
download(url, { 'Accept': 'image/webp,image/*,*/*;q=0.8' }, (err, imageBody) => {
if (err) logger.error(err)
callback(err, {
...response.data.result,
url: normalImage,
imageBody
})
setTimeout(() => cloudflareClient.delete(`/images/v1/${id}`), 60000)
})
}
}).catch((err) => callback(err))
}
}
Loading

0 comments on commit 6fe1b0d

Please sign in to comment.