forked from nuxt-contrib/netlify-ipx
-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: convert from plugin to library
- Loading branch information
Showing
12 changed files
with
510 additions
and
6,462 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,5 @@ | ||
[build] | ||
publish = "public/" | ||
publish = "public/" | ||
|
||
# [[plugins]] | ||
# package = "netlify-plugin-ipx" | ||
# [plugins.inputs] | ||
# domains = ['images.unsplash.com'] | ||
[functions] | ||
external_node_modules = ["fsevents"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
{ | ||
"private": true, | ||
"devDependencies": { | ||
"netlify-plugin-ipx": "latest" | ||
"@netlify/ipx": "link:.." | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
{ | ||
"rollup": { | ||
"externals": [ | ||
"fsevents" | ||
] | ||
} | ||
} | ||
|
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import { join } from 'path' | ||
import { createWriteStream, ensureDir, existsSync, unlink } from 'fs-extra' | ||
import fetch, { Headers } from 'node-fetch' | ||
import { createStorage } from 'unstorage' | ||
import fsDriver from 'unstorage/drivers/fs' | ||
import murmurhash from 'murmurhash' | ||
import etag from 'etag' | ||
import type { HandlerResponse } from '@netlify/functions' | ||
|
||
interface SourceMetadata { | ||
etag?: string; | ||
lastModified?: string; | ||
} | ||
|
||
const NOT_MODIFIED = 304 | ||
const GATEWAY_ERROR = 502 | ||
|
||
export interface SourceImageResult { | ||
response?: HandlerResponse | ||
cacheKey?: string; | ||
responseEtag?: string; | ||
} | ||
|
||
export async function loadSourceImage ({ cacheDir, url, requestEtag, modifiers, isLocal }): Promise<SourceImageResult> { | ||
const fileCache = join(cacheDir, 'cache') | ||
const metadataCache = join(cacheDir, 'metadata') | ||
|
||
await ensureDir(fileCache) | ||
await ensureDir(metadataCache) | ||
|
||
const metadataStore = createStorage({ | ||
driver: fsDriver({ base: metadataCache }) | ||
}) | ||
const cacheKey = String(murmurhash(url)) | ||
const inputCacheFile = join(fileCache, cacheKey) | ||
|
||
const headers = new Headers() | ||
let sourceMetadata: SourceMetadata | undefined | ||
if (existsSync(inputCacheFile)) { | ||
sourceMetadata = (await metadataStore.getItem(`source:${cacheKey}`)) as | ||
| SourceMetadata | ||
| undefined | ||
if (sourceMetadata) { | ||
// Ideally use etag | ||
if (sourceMetadata.etag) { | ||
headers.set('If-None-Match', sourceMetadata.etag) | ||
} else if (sourceMetadata.lastModified) { | ||
headers.set('If-Modified-Since', sourceMetadata.lastModified) | ||
} else { | ||
// If we have neither, the cachefile is useless | ||
await unlink(inputCacheFile) | ||
} | ||
} | ||
} | ||
|
||
let response | ||
try { | ||
response = await fetch(url, { | ||
headers | ||
}) | ||
} catch (e) { | ||
return { | ||
response: { | ||
statusCode: GATEWAY_ERROR, | ||
body: `Error loading source image: ${e.message}` | ||
} | ||
} | ||
} | ||
|
||
const sourceEtag = response.headers.get('etag') | ||
const sourceLastModified = response.headers.get('last-modified') | ||
const metadata = { | ||
etag: sourceEtag || sourceMetadata?.etag, | ||
lastModified: sourceLastModified || sourceMetadata?.lastModified | ||
} | ||
await metadataStore.setItem(`source:${cacheKey}`, metadata) | ||
// We try to contruct an etag without downloading or processing the image, but we need | ||
// either an etag or a last-modified date for the source image to do so. | ||
let responseEtag | ||
if (metadata.etag || metadata.lastModified) { | ||
responseEtag = etag(`${cacheKey}${metadata.etag || metadata.lastModified}${modifiers}`) | ||
if (requestEtag && (requestEtag === responseEtag)) { | ||
return { | ||
response: { | ||
statusCode: NOT_MODIFIED | ||
} | ||
} | ||
} | ||
} | ||
|
||
if (response.status === NOT_MODIFIED) { | ||
return { cacheKey, responseEtag } | ||
} | ||
if (!response.ok) { | ||
return { | ||
response: { | ||
statusCode: isLocal ? response.status : GATEWAY_ERROR, | ||
body: `Source image server responsed with ${response.status} ${response.statusText}` | ||
} | ||
} | ||
} | ||
|
||
if (!response.headers.get('content-type').startsWith('image/')) { | ||
return { | ||
response: { | ||
statusCode: GATEWAY_ERROR, | ||
body: 'Source is not an image' | ||
} | ||
} | ||
} | ||
|
||
const outfile = createWriteStream(inputCacheFile) | ||
await new Promise((resolve, reject) => { | ||
outfile.on('finish', resolve) | ||
outfile.on('error', reject) | ||
response.body.pipe(outfile) | ||
}) | ||
return { cacheKey, responseEtag } | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
import { join } from 'path' | ||
import { tmpdir } from 'os' | ||
import { createIPX, handleRequest, IPXOptions } from 'ipx' | ||
import { builder, Handler } from '@netlify/functions' | ||
import { parseURL } from 'ufo' | ||
import etag from 'etag' | ||
import { loadSourceImage } from './http' | ||
|
||
export function createIPXHandler ({ | ||
cacheDir = join(tmpdir(), 'ipx-cache'), | ||
...opts | ||
}: Partial<IPXOptions> & { cacheDir?: string } = {}) { | ||
const ipx = createIPX({ ...opts, dir: join(cacheDir, 'cache') }) | ||
|
||
const handler: Handler = async (event, _context) => { | ||
const host = event.headers.host | ||
const protocol = event.headers['x-forwarded-proto'] || 'http' | ||
let domains = opts.domains || [] | ||
const requestEtag = event.headers['if-none-match'] | ||
const url = event.path | ||
.replace('/.netlify/functions/ipx', '') | ||
.replace(/index\.htm$/, '') | ||
|
||
const [modifiers = '_', ...segments] = url.substr(1).split('/') | ||
let id = segments.join('/') | ||
|
||
const isLocal = !id.startsWith('http') | ||
if (isLocal) { | ||
id = `${protocol}://${host}/${id}` | ||
} else { | ||
if (typeof domains === 'string') { | ||
domains = (domains as string).split(',').map(s => s.trim()) | ||
} | ||
|
||
const hosts = domains.map(domain => parseURL(domain, 'https://').host) | ||
|
||
// Parse id as URL | ||
const parsedUrl = parseURL(id, 'https://') | ||
|
||
// Check host | ||
if (!parsedUrl.host) { | ||
return { | ||
statusCode: 403, | ||
body: 'Hostname is missing: ' + id | ||
} | ||
} | ||
if (!hosts.find(host => parsedUrl.host === host)) { | ||
return { | ||
statusCode: 403, | ||
body: 'Hostname is missing: ' + parsedUrl.host | ||
} | ||
} | ||
} | ||
|
||
const { response, cacheKey, responseEtag } = await loadSourceImage({ | ||
cacheDir, | ||
url: id, | ||
requestEtag, | ||
modifiers, | ||
isLocal | ||
}) | ||
|
||
if (response) { | ||
return response | ||
} | ||
|
||
const res = await handleRequest( | ||
{ | ||
url: `/${modifiers}/${cacheKey}`, | ||
headers: event.headers | ||
}, | ||
ipx | ||
) | ||
|
||
const body = | ||
typeof res.body === 'string' ? res.body : res.body.toString('base64') | ||
|
||
res.headers.etag = responseEtag || etag(body) | ||
|
||
return { | ||
statusCode: res.statusCode, | ||
message: res.statusMessage, | ||
headers: res.headers, | ||
isBase64Encoded: typeof res.body !== 'string', | ||
body | ||
} | ||
} | ||
|
||
return builder(handler) | ||
} |
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,4 +5,4 @@ | |
"moduleResolution": "Node", | ||
"esModuleInterop": true | ||
} | ||
} | ||
} |
Oops, something went wrong.