Skip to content
Snippets Groups Projects
Unverified Commit 66c7f13e authored by Paul Melnikow's avatar Paul Melnikow Committed by GitHub
Browse files

Drop gif + png, and redirect png to raster.shields.io (#3644)

1. Remove rasterization support from the server. This responsibility is delegated to a raster server which proxies the SVG badges and renders them.
2. When a raster server URL is configured, 301 redirect all .png badges to the identical URL on the raster server.
    `https://img.shields.io/npm/v/express.png?style=flat-square` :arrow_right_hook:`https://raster.shields.io/npm/v/express.png?style=flat-square`
3. For configured redirects, redirect to the canonical URL on the raster server.
    `https://img.shields.io/vso/build/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/2.png?style=flat-square`
    :arrow_right_hook:`https://img.shields.io/azure-devops/build/totodem/8cf3ec0e-d0c2-4fcd-8206-ad204f254a96/2.png?style=flat-square`
4. Redirect the "legacy badge old version" to the appropriate URL on the raster server.
5. When no raster server is configured (e.g. PRs), render an SVG containing **404 | raster badges not available** for all `.png` badges. (Note that the raster server can be self-hosted; however, this is deferred to a later PR.)
5. Drop support for jpg and gif which are very infrequently used (see #3112). Render an SVG containing **410 | jpg no longer available**.
7. ~~Remove raster dependencies.~~ Remove the raster cache (which is only used in the CLI, and therefore pointless).
8. Move the LRUCache code out of the npm package.
8. A wee bit of refactoring in `server.js`.

Ref #3112
Close #3631
parent c6ef885b
No related branches found
No related tags found
No related merge requests found
Showing
with 166 additions and 135 deletions
FROM node:8-alpine FROM node:8-alpine
RUN apk add --no-cache gettext imagemagick librsvg git
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
RUN mkdir /usr/src/app/private RUN mkdir /usr/src/app/private
WORKDIR /usr/src/app WORKDIR /usr/src/app
......
...@@ -14,6 +14,8 @@ public: ...@@ -14,6 +14,8 @@ public:
redirectUri: 'REDIRECT_URI' redirectUri: 'REDIRECT_URI'
rasterUrl: 'RASTER_URL'
cors: cors:
allowedOrigin: allowedOrigin:
__name: 'ALLOWED_ORIGIN' __name: 'ALLOWED_ORIGIN'
......
...@@ -11,6 +11,8 @@ public: ...@@ -11,6 +11,8 @@ public:
redirectUrl: 'https://shields.io/' redirectUrl: 'https://shields.io/'
rasterUrl: 'https://raster.shields.io'
private: private:
# These are not really private; they should be moved to `public`. # These are not really private; they should be moved to `public`.
shields_ips: ['192.99.59.72', '51.254.114.150', '149.56.96.133'] shields_ips: ['192.99.59.72', '51.254.114.150', '149.56.96.133']
...@@ -7,4 +7,6 @@ public: ...@@ -7,4 +7,6 @@ public:
redirectUrl: 'http://badge-server.example.com' redirectUrl: 'http://badge-server.example.com'
rasterUrl: 'http://raster.example.com'
handleInternalErrors: false handleInternalErrors: false
'use strict' 'use strict'
const { URL } = require('url')
const queryString = require('query-string') const queryString = require('query-string')
const pathToRegexp = require('path-to-regexp') const pathToRegexp = require('path-to-regexp')
...@@ -134,6 +135,15 @@ function dynamicBadgeUrl({ ...@@ -134,6 +135,15 @@ function dynamicBadgeUrl({
return `${baseUrl}/badge/dynamic/${datatype}.${format}?${outQueryString}` return `${baseUrl}/badge/dynamic/${datatype}.${format}?${outQueryString}`
} }
function rasterRedirectUrl({ rasterUrl }, badgeUrl) {
// Ensure we're always using the `rasterUrl` by using just the path from
// the request URL.
const { pathname, search } = new URL(badgeUrl, 'https://bogus.test')
const result = new URL(pathname, rasterUrl)
result.search = search
return result
}
module.exports = { module.exports = {
badgeUrlFromPath, badgeUrlFromPath,
badgeUrlFromPattern, badgeUrlFromPattern,
...@@ -141,4 +151,5 @@ module.exports = { ...@@ -141,4 +151,5 @@ module.exports = {
staticBadgeUrl, staticBadgeUrl,
queryStringStaticBadgeUrl, queryStringStaticBadgeUrl,
dynamicBadgeUrl, dynamicBadgeUrl,
rasterRedirectUrl,
} }
...@@ -316,7 +316,7 @@ describe('BaseService', function() { ...@@ -316,7 +316,7 @@ describe('BaseService', function() {
}) })
describe('ScoutCamp integration', function() { describe('ScoutCamp integration', function() {
const expectedRouteRegex = /^\/foo\/([^/]+?)\.(svg|png|gif|jpg|json)$/ const expectedRouteRegex = /^\/foo\/([^/]+?)\.(svg|json)$/
let mockCamp let mockCamp
let mockHandleRequest let mockHandleRequest
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
const domain = require('domain') const domain = require('domain')
const request = require('request') const request = require('request')
const queryString = require('query-string') const queryString = require('query-string')
const LruCache = require('../../gh-badges/lib/lru-cache')
const makeBadge = require('../../gh-badges/lib/make-badge') const makeBadge = require('../../gh-badges/lib/make-badge')
const log = require('../server/log') const log = require('../server/log')
const { setCacheHeaders } = require('./cache-headers') const { setCacheHeaders } = require('./cache-headers')
...@@ -14,6 +13,7 @@ const { ...@@ -14,6 +13,7 @@ const {
ShieldsRuntimeError, ShieldsRuntimeError,
} = require('./errors') } = require('./errors')
const { makeSend } = require('./legacy-result-sender') const { makeSend } = require('./legacy-result-sender')
const LruCache = require('./lru-cache')
const coalesceBadge = require('./coalesce-badge') const coalesceBadge = require('./coalesce-badge')
// We avoid calling the vendor's server for computation of the information in a // We avoid calling the vendor's server for computation of the information in a
......
'use strict' 'use strict'
const fs = require('fs')
const path = require('path')
const stream = require('stream') const stream = require('stream')
const svg2img = require('../../gh-badges/lib/svg-to-img')
const log = require('../server/log')
const internalError = fs.readFileSync(
path.resolve(__dirname, '..', 'server', 'error-pages', '500.html'),
'utf-8'
)
function streamFromString(str) { function streamFromString(str) {
const newStream = new stream.Readable() const newStream = new stream.Readable()
...@@ -25,21 +16,6 @@ function sendSVG(res, askres, end) { ...@@ -25,21 +16,6 @@ function sendSVG(res, askres, end) {
end(null, { template: streamFromString(res) }) end(null, { template: streamFromString(res) })
} }
function sendOther(format, res, askres, end) {
askres.setHeader('Content-Type', `image/${format}`)
svg2img(res, format)
// This interacts with callback code and can't use async/await.
// eslint-disable-next-line promise/prefer-await-to-then
.then(data => {
end(null, { template: streamFromString(data) })
})
.catch(err => {
// This emits status code 200, though 500 would be preferable.
log.error('svg2img error', err)
end(internalError)
})
}
function sendJSON(res, askres, end) { function sendJSON(res, askres, end) {
askres.setHeader('Content-Type', 'application/json') askres.setHeader('Content-Type', 'application/json')
askres.setHeader('Access-Control-Allow-Origin', '*') askres.setHeader('Access-Control-Allow-Origin', '*')
...@@ -52,7 +28,7 @@ function makeSend(format, askres, end) { ...@@ -52,7 +28,7 @@ function makeSend(format, askres, end) {
} else if (format === 'json') { } else if (format === 'json') {
return res => sendJSON(res, askres, end) return res => sendJSON(res, askres, end)
} else { } else {
return res => sendOther(format, res, askres, end) throw Error(`Unrecognized format: ${format}`)
} }
} }
......
File moved
...@@ -62,8 +62,11 @@ module.exports = function redirector(attrs) { ...@@ -62,8 +62,11 @@ module.exports = function redirector(attrs) {
return route return route
} }
static register({ camp, requestCounter }) { static register({ camp, requestCounter }, { rasterUrl }) {
const { regex, captureNames } = prepareRoute(this.route) const { regex, captureNames } = prepareRoute({
...this.route,
withPng: Boolean(rasterUrl),
})
const serviceRequestCounter = this._createServiceRequestCounter({ const serviceRequestCounter = this._createServiceRequestCounter({
requestCounter, requestCounter,
...@@ -104,7 +107,9 @@ module.exports = function redirector(attrs) { ...@@ -104,7 +107,9 @@ module.exports = function redirector(attrs) {
// The final capture group is the extension. // The final capture group is the extension.
const format = match.slice(-1)[0] const format = match.slice(-1)[0]
const redirectUrl = `${targetPath}.${format}${urlSuffix}` const redirectUrl = `${
format === 'png' ? rasterUrl : ''
}${targetPath}.${format}${urlSuffix}`
trace.logTrace('outbound', emojic.shield, 'Redirect URL', redirectUrl) trace.logTrace('outbound', emojic.shield, 'Redirect URL', redirectUrl)
ask.res.statusCode = 301 ask.res.statusCode = 301
......
...@@ -75,7 +75,10 @@ describe('Redirector', function() { ...@@ -75,7 +75,10 @@ describe('Redirector', function() {
transformPath, transformPath,
dateAdded, dateAdded,
}) })
ServiceClass.register({ camp }, {}) ServiceClass.register(
{ camp },
{ rasterUrl: 'http://raster.example.test' }
)
}) })
it('should redirect as configured', async function() { it('should redirect as configured', async function() {
...@@ -90,7 +93,7 @@ describe('Redirector', function() { ...@@ -90,7 +93,7 @@ describe('Redirector', function() {
expect(headers.location).to.equal('/new/service/hello-world.svg') expect(headers.location).to.equal('/new/service/hello-world.svg')
}) })
it('should preserve the extension', async function() { it('should redirect raster extensions to the canonical path as configured', async function() {
const { statusCode, headers } = await got( const { statusCode, headers } = await got(
`${baseUrl}/very/old/service/hello-world.png`, `${baseUrl}/very/old/service/hello-world.png`,
{ {
...@@ -99,7 +102,9 @@ describe('Redirector', function() { ...@@ -99,7 +102,9 @@ describe('Redirector', function() {
) )
expect(statusCode).to.equal(301) expect(statusCode).to.equal(301)
expect(headers.location).to.equal('/new/service/hello-world.png') expect(headers.location).to.equal(
'http://raster.example.test/new/service/hello-world.png'
)
}) })
it('should forward the query params', async function() { it('should forward the query params', async function() {
......
...@@ -26,18 +26,16 @@ function assertValidRoute(route, message = undefined) { ...@@ -26,18 +26,16 @@ function assertValidRoute(route, message = undefined) {
Joi.assert(route, isValidRoute, message) Joi.assert(route, isValidRoute, message)
} }
function prepareRoute({ base, pattern, format, capture }) { function prepareRoute({ base, pattern, format, capture, withPng }) {
const extensionRegex = ['svg', 'json']
.concat(withPng ? ['png'] : [])
.join('|')
let regex, captureNames let regex, captureNames
if (pattern === undefined) { if (pattern === undefined) {
regex = new RegExp( regex = new RegExp(`^${makeFullUrl(base, format)}\\.(${extensionRegex})$`)
`^${makeFullUrl(base, format)}\\.(svg|png|gif|jpg|json)$`
)
captureNames = capture || [] captureNames = capture || []
} else { } else {
const fullPattern = `${makeFullUrl( const fullPattern = `${makeFullUrl(base, pattern)}.:ext(${extensionRegex})`
base,
pattern
)}.:ext(svg|png|gif|jpg|json)`
const keys = [] const keys = []
regex = pathToRegexp(fullPattern, keys, { regex = pathToRegexp(fullPattern, keys, {
strict: true, strict: true,
......
...@@ -33,9 +33,6 @@ describe('Route helpers', function() { ...@@ -33,9 +33,6 @@ describe('Route helpers', function() {
test(namedParams, () => { test(namedParams, () => {
forCases([ forCases([
given('/foo/bar.bar.bar.svg'), given('/foo/bar.bar.bar.svg'),
given('/foo/bar.bar.bar.png'),
given('/foo/bar.bar.bar.gif'),
given('/foo/bar.bar.bar.jpg'),
given('/foo/bar.bar.bar.json'), given('/foo/bar.bar.bar.json'),
]).expect({ namedParamA: 'bar.bar.bar' }) ]).expect({ namedParamA: 'bar.bar.bar' })
}) })
...@@ -64,9 +61,6 @@ describe('Route helpers', function() { ...@@ -64,9 +61,6 @@ describe('Route helpers', function() {
test(namedParams, () => { test(namedParams, () => {
forCases([ forCases([
given('/foo/bar.bar.bar.svg'), given('/foo/bar.bar.bar.svg'),
given('/foo/bar.bar.bar.png'),
given('/foo/bar.bar.bar.gif'),
given('/foo/bar.bar.bar.jpg'),
given('/foo/bar.bar.bar.json'), given('/foo/bar.bar.bar.json'),
]).expect({ namedParamA: 'bar.bar.bar' }) ]).expect({ namedParamA: 'bar.bar.bar' })
}) })
...@@ -83,9 +77,6 @@ describe('Route helpers', function() { ...@@ -83,9 +77,6 @@ describe('Route helpers', function() {
test(namedParams, () => { test(namedParams, () => {
forCases([ forCases([
given('/foo/bar.bar.bar.svg'), given('/foo/bar.bar.bar.svg'),
given('/foo/bar.bar.bar.png'),
given('/foo/bar.bar.bar.gif'),
given('/foo/bar.bar.bar.jpg'),
given('/foo/bar.bar.bar.json'), given('/foo/bar.bar.bar.json'),
]).expect({}) ]).expect({})
}) })
......
...@@ -16,7 +16,10 @@ const { ...@@ -16,7 +16,10 @@ const {
clearRequestCache, clearRequestCache,
} = require('../base-service/legacy-request-handler') } = require('../base-service/legacy-request-handler')
const { clearRegularUpdateCache } = require('../legacy/regular-update') const { clearRegularUpdateCache } = require('../legacy/regular-update')
const { staticBadgeUrl } = require('../badge-urls/make-badge-url') const {
staticBadgeUrl,
rasterRedirectUrl,
} = require('../badge-urls/make-badge-url')
const log = require('./log') const log = require('./log')
const sysMonitor = require('./monitor') const sysMonitor = require('./monitor')
const PrometheusMetrics = require('./prometheus-metrics') const PrometheusMetrics = require('./prometheus-metrics')
...@@ -52,6 +55,7 @@ const publicConfigSchema = Joi.object({ ...@@ -52,6 +55,7 @@ const publicConfigSchema = Joi.object({
cert: Joi.string(), cert: Joi.string(),
}, },
redirectUrl: optionalUrl, redirectUrl: optionalUrl,
rasterUrl: optionalUrl,
cors: { cors: {
allowedOrigin: Joi.array() allowedOrigin: Joi.array()
.items(optionalUrl) .items(optionalUrl)
...@@ -183,16 +187,43 @@ module.exports = class Server { ...@@ -183,16 +187,43 @@ module.exports = class Server {
* Set up Scoutcamp routes for 404/not found responses * Set up Scoutcamp routes for 404/not found responses
*/ */
registerErrorHandlers() { registerErrorHandlers() {
const { camp } = this const { camp, config } = this
const {
public: { rasterUrl },
} = config
camp.notfound(/\.(gif|jpg)$/, (query, match, end, request) => {
const [, format] = match
makeSend('svg', request.res, end)(
makeBadge({
text: ['410', `${format} no longer available`],
color: 'lightgray',
format: 'svg',
})
)
})
if (!rasterUrl) {
camp.notfound(/\.png$/, (query, match, end, request) => {
makeSend('svg', request.res, end)(
makeBadge({
text: ['404', 'raster badges not available'],
color: 'lightgray',
format: 'svg',
})
)
})
}
camp.notfound(/\.(svg|png|gif|jpg|json)/, (query, match, end, request) => { camp.notfound(/\.(svg|json)$/, (query, match, end, request) => {
const [, format] = match const [, format] = match
const svg = makeBadge({ makeSend(format, request.res, end)(
makeBadge({
text: ['404', 'badge not found'], text: ['404', 'badge not found'],
color: 'red', color: 'red',
format, format,
}) })
makeSend(format, request.res, end)(svg) )
}) })
camp.notfound(/.*/, (query, match, end, request) => { camp.notfound(/.*/, (query, match, end, request) => {
...@@ -219,6 +250,7 @@ module.exports = class Server { ...@@ -219,6 +250,7 @@ module.exports = class Server {
cacheHeaders: config.public.cacheHeaders, cacheHeaders: config.public.cacheHeaders,
profiling: config.public.profiling, profiling: config.public.profiling,
fetchLimitBytes: bytes(config.public.fetchLimit), fetchLimitBytes: bytes(config.public.fetchLimit),
rasterUrl: config.public.rasterUrl,
} }
) )
) )
...@@ -234,13 +266,18 @@ module.exports = class Server { ...@@ -234,13 +266,18 @@ module.exports = class Server {
*/ */
registerRedirects() { registerRedirects() {
const { config, camp } = this const { config, camp } = this
const {
public: { rasterUrl, redirectUrl },
} = config
// Any badge, old version. This route must be registered last. if (rasterUrl) {
// Any badge, old version.
camp.route(/^\/([^/]+)\/(.+).png$/, (queryParams, match, end, ask) => { camp.route(/^\/([^/]+)\/(.+).png$/, (queryParams, match, end, ask) => {
const [, label, message] = match const [, label, message] = match
const { color } = queryParams const { color } = queryParams
const redirectUrl = staticBadgeUrl({ const redirectUrl = staticBadgeUrl({
baseUrl: rasterUrl,
label, label,
message, message,
// Fixes https://github.com/badges/shields/issues/3260 // Fixes https://github.com/badges/shields/issues/3260
...@@ -258,10 +295,28 @@ module.exports = class Server { ...@@ -258,10 +295,28 @@ module.exports = class Server {
ask.res.end() ask.res.end()
}) })
if (config.public.redirectUrl) { // Redirect to the raster server for raster versions of modern badges.
camp.route(/\.png$/, (queryParams, match, end, ask) => {
ask.res.statusCode = 301
ask.res.setHeader(
'Location',
rasterRedirectUrl({ rasterUrl }, ask.req.url)
)
// The redirect is permanent, though let's start off with a shorter
// cache time in case we've made mistakes.
// const cacheDuration = (365 * 24 * 3600) | 0 // 1 year
const cacheDuration = 3600 | 0 // 1 hour
ask.res.setHeader('Cache-Control', `max-age=${cacheDuration}`)
ask.res.end()
})
}
if (redirectUrl) {
camp.route(/^\/$/, (data, match, end, ask) => { camp.route(/^\/$/, (data, match, end, ask) => {
ask.res.statusCode = 302 ask.res.statusCode = 302
ask.res.setHeader('Location', config.public.redirectUrl) ask.res.setHeader('Location', redirectUrl)
ask.res.end() ask.res.end()
}) })
} }
......
'use strict' 'use strict'
const fs = require('fs')
const path = require('path')
const { expect } = require('chai') const { expect } = require('chai')
const isPng = require('is-png')
const isSvg = require('is-svg') const isSvg = require('is-svg')
const sinon = require('sinon')
const portfinder = require('portfinder') const portfinder = require('portfinder')
const svg2img = require('../../gh-badges/lib/svg-to-img')
const got = require('../got-test-client') const got = require('../got-test-client')
const { createTestServer } = require('./in-process-server-test-helpers') const { createTestServer } = require('./in-process-server-test-helpers')
...@@ -37,12 +32,17 @@ describe('The server', function() { ...@@ -37,12 +32,17 @@ describe('The server', function() {
.and.to.include('apple') .and.to.include('apple')
}) })
it('should produce colorscheme PNG badges', async function() { it('should redirect colorscheme PNG badges as configured', async function() {
const { statusCode, body } = await got(`${baseUrl}:fruit-apple-green.png`, { const { statusCode, headers } = await got(
encoding: null, `${baseUrl}:fruit-apple-green.png`,
}) {
expect(statusCode).to.equal(200) followRedirect: false,
expect(body).to.satisfy(isPng) }
)
expect(statusCode).to.equal(301)
expect(headers.location).to.equal(
'http://raster.example.com/:fruit-apple-green.png'
)
}) })
it('should preserve label case', async function() { it('should preserve label case', async function() {
...@@ -118,28 +118,14 @@ describe('The server', function() { ...@@ -118,28 +118,14 @@ describe('The server', function() {
expect(headers.location).to.equal('http://badge-server.example.com') expect(headers.location).to.equal('http://badge-server.example.com')
}) })
context('with svg2img error', function() { it('should return the 410 badge for obsolete formats', async function() {
const expectedError = fs.readFileSync( const { statusCode, body } = await got(`${baseUrl}npm/v/express.jpg`, {
path.resolve(__dirname, 'error-pages', '500.html') throwHttpErrors: false,
)
let toBufferStub
beforeEach(function() {
toBufferStub = sinon
.stub(svg2img._imageMagick.prototype, 'toBuffer')
.callsArgWith(1, Error('whoops'))
})
afterEach(function() {
toBufferStub.restore()
})
it('should emit the 500 message', async function() {
const { statusCode, body } = await got(
`${baseUrl}:some_new-badge-green.png`
)
// This emits status code 200, though 500 would be preferable.
expect(statusCode).to.equal(200)
expect(body).to.include(expectedError)
}) })
expect(statusCode).to.equal(404)
expect(body)
.to.satisfy(isSvg)
.and.to.include('410')
.and.to.include('jpg no longer available')
}) })
}) })
...@@ -42,11 +42,6 @@ install node and npm: https://nodejs.org/en/download/ ...@@ -42,11 +42,6 @@ install node and npm: https://nodejs.org/en/download/
In case you get the _"getaddrinfo ENOTFOUND localhost"_ error, visit [http://127.0.0.1:3000/](http://127.0.0.1:3000) instead or take a look at [this issue](https://github.com/angular/angular-cli/issues/2227#issuecomment-358036526). In case you get the _"getaddrinfo ENOTFOUND localhost"_ error, visit [http://127.0.0.1:3000/](http://127.0.0.1:3000) instead or take a look at [this issue](https://github.com/angular/angular-cli/issues/2227#issuecomment-358036526).
You may also want to install
[ImageMagick](https://www.imagemagick.org/script/download.php).
This is an optional dependency needed for generating badges in raster format,
but you can get a dev copy running without it.
## (3) Open an Issue ## (3) Open an Issue
Before you want to implement your service, you may want to [open an issue](https://github.com/badges/shields/issues/new?template=3_Badge_request.md) and describe what you have in mind: Before you want to implement your service, you may want to [open an issue](https://github.com/badges/shields/issues/new?template=3_Badge_request.md) and describe what you have in mind:
......
...@@ -73,12 +73,10 @@ Shields has mercifully little persistent state: ...@@ -73,12 +73,10 @@ Shields has mercifully little persistent state:
inspectable. inspectable.
- The [request cache][] - The [request cache][]
- The [regular-update cache][] - The [regular-update cache][]
- The [raster cache][]
[github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js [github auth admin endpoint]: https://github.com/badges/shields/blob/master/services/github/auth/admin.js
[request cache]: https://github.com/badges/shields/blob/master/core/base-service/legacy-request-handler.js#L29-L30 [request cache]: https://github.com/badges/shields/blob/master/core/base-service/legacy-request-handler.js#L29-L30
[regular-update cache]: https://github.com/badges/shields/blob/master/core/legacy/regular-update.js [regular-update cache]: https://github.com/badges/shields/blob/master/core/legacy/regular-update.js
[raster cache]: https://github.com/badges/shields/blob/master/gh-badges/lib/svg-to-img.js#L9-L10
[oauth transfer]: https://developer.github.com/apps/managing-oauth-apps/transferring-ownership-of-an-oauth-app/ [oauth transfer]: https://developer.github.com/apps/managing-oauth-apps/transferring-ownership-of-an-oauth-app/
## Configuration ## Configuration
......
...@@ -93,6 +93,27 @@ machine. ...@@ -93,6 +93,27 @@ machine.
[shields.example.env]: ../shields.example.env [shields.example.env]: ../shields.example.env
## Raster server
If you want to host PNG badges, you can also self-host a [raster server][]
which points to your badge server. It's designed as a web function which is
tested on Zeit Now, though you may be able to run it on AWS Lambda. It's
built on the [micro][] framework, and comes with a `start` script that allows
it to run as a standalone Node service.
- In your raster instance, set `BASE_URL` to your Shields instance, e.g.
`https://shields.example.co`.
- Optionally, in your Shields, instance, configure `RASTER_URL` to the base
URL, e.g. `https://raster.example.co`. This will send 301 redirects
for the legacy raster URLs instead of 404's.
If anyone has set this up, more documentation on how to do this would be
welcome! It would also be nice to ship a Docker image that includes a
preconfigured raster server.
[raster server]: https://github.com/badges/svg-to-image-proxy
[micro]: https://github.com/zeit/micro
## Zeit Now ## Zeit Now
To deploy using Zeit Now: To deploy using Zeit Now:
......
...@@ -2,20 +2,10 @@ ...@@ -2,20 +2,10 @@
const { promisify } = require('util') const { promisify } = require('util')
const gm = require('gm') const gm = require('gm')
const LruCache = require('./lru-cache')
const imageMagick = gm.subClass({ imageMagick: true }) const imageMagick = gm.subClass({ imageMagick: true })
// The following is an arbitrary limit (~1.5MB, 1.5kB/image).
const imgCache = new LruCache(1000)
async function svgToImg(svg, format) { async function svgToImg(svg, format) {
const cacheIndex = `${format}${svg}`
if (imgCache.has(cacheIndex)) {
return imgCache.get(cacheIndex)
}
const svgBuffer = Buffer.from( const svgBuffer = Buffer.from(
`<?xml version="1.0" encoding="UTF-8" standalone="yes"?>${svg}` `<?xml version="1.0" encoding="UTF-8" standalone="yes"?>${svg}`
) )
...@@ -26,14 +16,10 @@ async function svgToImg(svg, format) { ...@@ -26,14 +16,10 @@ async function svgToImg(svg, format) {
.flatten() .flatten()
const toBuffer = chain.toBuffer.bind(chain) const toBuffer = chain.toBuffer.bind(chain)
const data = await promisify(toBuffer)(format) return promisify(toBuffer)(format)
imgCache.set(cacheIndex, data)
return data
} }
module.exports = svgToImg module.exports = svgToImg
// To simplify testing. // To simplify testing.
module.exports._imgCache = imgCache
module.exports._imageMagick = imageMagick module.exports._imageMagick = imageMagick
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment