From 7cf888befc6db14eadf2a6279190ffdc39e04ff2 Mon Sep 17 00:00:00 2001 From: Conduitry Date: Wed, 12 Aug 2020 11:28:37 -0400 Subject: [PATCH] in exporting, handle binary files from server routes - follow , , when crawling - don't corrupt binary responses from server routes Co-authored-by: ruben Co-authored-by: Andre Loker <140714+aloker@users.noreply.github.com> --- .../middleware/get_server_route_handler.ts | 2 +- src/api/export.ts | 66 +++++++++++++++--- test/apps/export/content/example-192.png | Bin 0 -> 5334 bytes test/apps/export/content/example-512.png | Bin 0 -> 15358 bytes test/apps/export/content/test.pdf | Bin 0 -> 5681 bytes test/apps/export/src/routes/blog/[slug].html | 13 +++- test/apps/export/src/routes/blog/_posts.js | 4 +- test/apps/export/src/routes/img/[slug].png.js | 15 ++++ .../apps/export/src/routes/pdfs/[slug].pdf.js | 15 ++++ test/apps/export/test.ts | 25 +++++-- test/unit/srcset/test.ts | 30 ++++++++ 11 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 test/apps/export/content/example-192.png create mode 100644 test/apps/export/content/example-512.png create mode 100644 test/apps/export/content/test.pdf create mode 100644 test/apps/export/src/routes/img/[slug].png.js create mode 100644 test/apps/export/src/routes/pdfs/[slug].pdf.js create mode 100644 test/unit/srcset/test.ts diff --git a/runtime/src/server/middleware/get_server_route_handler.ts b/runtime/src/server/middleware/get_server_route_handler.ts index 58c985668..e66035157 100644 --- a/runtime/src/server/middleware/get_server_route_handler.ts +++ b/runtime/src/server/middleware/get_server_route_handler.ts @@ -37,7 +37,7 @@ export function get_server_route_handler(routes: ServerRoute[]) { method: req.method, status: res.statusCode, type: headers['content-type'], - body: Buffer.concat(chunks).toString() + body: Buffer.concat(chunks) }); }; } diff --git a/src/api/export.ts b/src/api/export.ts index 2d88f2d24..eba1f2dac 100644 --- a/src/api/export.ts +++ b/src/api/export.ts @@ -50,6 +50,29 @@ function get_href(attrs: string) { return match && (match[1] || match[2] || match[3]); } +function get_src(attrs: string) { + const match = /src\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]*))/.exec(attrs); + return match && (match[1] || match[2] || match[3]); +} + +export function get_srcset_urls(attrs: string) { + const results: string[] = []; + // Note that the srcset allows any ASCII whitespace, including newlines. + const match = /srcset\s*=\s*(?:"(.*?)"|'(.*?)'|([^\s>]*))/s.exec(attrs); + if (match) { + const attr_content = match[1] || match[2] || match[3]; + // Parse the content of the srcset attribute. + // The regexp is modelled after the srcset specs (https://html.spec.whatwg.org/multipage/images.html#srcset-attribute) + // and should cover most reasonable cases. + const regex = /\s*([^\s,]\S+[^\s,])\s*((?:\d+w)|(?:-?\d+(?:\.\d+)?(?:[eE]-?\d+)?x))?/gm; + let sub_matches; + while (sub_matches = regex.exec(attr_content)) { + results.push(sub_matches[1]); + } + } + return results; +} + export { _export as export }; async function _export({ @@ -109,7 +132,7 @@ async function _export({ const seen = new Set(); const saved = new Set(); - function save(url: string, status: number, type: string, body: string) { + function save(url: string, status: number, type: string, body: string | ArrayBuffer) { const { pathname } = resolve(origin, url); let file = decodeURIComponent(pathname.slice(1)); @@ -122,12 +145,19 @@ async function _export({ if (!file.endsWith('.html')) { file = file === '' ? 'index.html' : `${file}/index.html`; } - body = minify_html(body); + + if (typeof body === 'string') { + body = minify_html(body); + } else { + oninfo({ message: `Content of {url} has content-type text/html but the content was received as a binary buffer. The HTML will not be minified.` }); + } } + const buffer = Buffer.from(body); + onfile({ file, - size: body.length, + size: buffer.byteLength, status }); @@ -135,7 +165,7 @@ async function _export({ if (fs.existsSync(export_file)) return; mkdirp(path.dirname(export_file)); - return writeFile(export_file, body); + return writeFile(export_file, buffer); } function handle(url: URL, fetchOpts: FetchOpts, addCallback: Function) { @@ -184,7 +214,9 @@ async function _export({ let type = response.headers.get('Content-Type'); - let body = await response.text(); + let body = type.startsWith('text/') + ? await response.text() + : await response.arrayBuffer(); const range = ~~(response.status / 100); @@ -193,26 +225,38 @@ async function _export({ const link = parseLinkHeader(response.headers.get('Link') || ''); link.refs.forEach((ref: Ref) => { if (ref.rel === 'preload') { - body = body.replace('', + body = (body as string).replace('', ``); } }); if (pathname !== '/service-worker-index.html') { - const cleaned = clean_html(body); + const cleaned = clean_html(body as string); const base_match = //m.exec(cleaned); const base_href = base_match && get_href(base_match[1]); const base = resolve(url.href, base_href); let match; - const pattern = //gm; + const pattern = /<(a|img|source)\s+([\s\S]+?)>/gm; while (match = pattern.exec(cleaned)) { - const attrs = match[1]; - const href = get_href(attrs); + let hrefs: string[] = []; + const element = match[1]; + const attrs = match[2]; + + if (element === 'a') { + hrefs.push(get_href(attrs)); + } else { + if (element === 'img') { + hrefs.push(get_src(attrs)); + } + hrefs.push.apply(hrefs, get_srcset_urls(attrs)); + } + + hrefs = hrefs.filter(Boolean); - if (href) { + for (const href of hrefs) { const url = resolve(base.href, href); if (url.protocol === protocol && url.host === host) { diff --git a/test/apps/export/content/example-192.png b/test/apps/export/content/example-192.png new file mode 100644 index 0000000000000000000000000000000000000000..0a9e4b147e0e398ed739a6445dee3c480c67f97c GIT binary patch literal 5334 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

$wgR5 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 M4&dQY*M$Q92deKvQUCw| literal 0 HcmV?d00001 diff --git a/test/apps/export/src/routes/blog/[slug].html b/test/apps/export/src/routes/blog/[slug].html index 2febdf20d..b00b43c1f 100644 --- a/test/apps/export/src/routes/blog/[slug].html +++ b/test/apps/export/src/routes/blog/[slug].html @@ -10,4 +10,15 @@ export let post; -

{post.title}

\ No newline at end of file +

{post.title}

+ +{#if post.src} + + + + +{/if} + +{#if post.pdf} +
{post.pdf} +{/if} diff --git a/test/apps/export/src/routes/blog/_posts.js b/test/apps/export/src/routes/blog/_posts.js index d283aa6cf..c3f209651 100644 --- a/test/apps/export/src/routes/blog/_posts.js +++ b/test/apps/export/src/routes/blog/_posts.js @@ -1,5 +1,5 @@ export default [ - { slug: 'foo', title: 'once upon a foo' }, + { slug: 'foo', title: 'once upon a foo', src: 'img/example-512.png', srcset: 'img/example-512.png 512w, img/example-192.png 192w', pdf: 'pdfs/test.pdf' }, { slug: 'bar', title: 'a bar is born' }, { slug: 'baz', title: 'bazzily ever after' } -]; \ No newline at end of file +]; diff --git a/test/apps/export/src/routes/img/[slug].png.js b/test/apps/export/src/routes/img/[slug].png.js new file mode 100644 index 000000000..2644a820e --- /dev/null +++ b/test/apps/export/src/routes/img/[slug].png.js @@ -0,0 +1,15 @@ +const fs = require('fs'); +const path = require('path'); +const cwd = process.cwd(); + +export function get(req, res) { + + const { slug } = req.params; + const image = path.join(cwd, `/content/${slug}.png`); + + let s = fs.createReadStream(image); + s.on('open', () => { + res.writeHead(200, { 'Content-Type': 'image/png' }); + s.pipe(res); + }); +} diff --git a/test/apps/export/src/routes/pdfs/[slug].pdf.js b/test/apps/export/src/routes/pdfs/[slug].pdf.js new file mode 100644 index 000000000..f9c5a1b03 --- /dev/null +++ b/test/apps/export/src/routes/pdfs/[slug].pdf.js @@ -0,0 +1,15 @@ +const fs = require('fs'); +const path = require('path'); +const cwd = process.cwd(); + +export function get(req, res, next) { + + const { slug } = req.params; + const image = path.join(cwd, `/content/${slug}.pdf`); + + let s = fs.createReadStream(image); + s.on('open', () => { + res.writeHead(200, { 'Content-Type': 'application/pdf' }); + s.pipe(res); + }); +} diff --git a/test/apps/export/test.ts b/test/apps/export/test.ts index 850e38635..1b74e9b23 100644 --- a/test/apps/export/test.ts +++ b/test/apps/export/test.ts @@ -42,15 +42,32 @@ describe('export', function() { 'service-worker-index.html', 'service-worker.js', 'test.pdf', + 'img/example-192.png', + 'img/example-512.png', + 'pdfs/test.pdf', ...boom ].sort()); }); it('does not corrupt binary file links (like pdf)', () => { - const input = readFileSync(`${__dirname}/static/test.pdf`) - const output = readFileSync(`${__dirname}/__sapper__/export/test.pdf`) - assert.ok(input.equals(output)) - }) + const input = readFileSync(`${__dirname}/static/test.pdf`); + const output = readFileSync(`${__dirname}/__sapper__/export/test.pdf`); + assert.ok(input.equals(output)); + }); + + it('does not corrupt image files from server routes', () => { + for(const file of ['example-192.png', 'example-512.png']) { + const input = readFileSync(`${__dirname}/content/${file}`); + const output = readFileSync(`${__dirname}/__sapper__/export/img/${file}`); + assert.ok(input.equals(output)); + } + }); + + it('does not corrupt pdf files from server routes', () => { + const input = readFileSync(`${__dirname}/content/test.pdf`); + const output = readFileSync(`${__dirname}/__sapper__/export/pdfs/test.pdf`); + assert.ok(input.equals(output)); + }); // TODO test timeout, basepath }); diff --git a/test/unit/srcset/test.ts b/test/unit/srcset/test.ts new file mode 100644 index 000000000..2e8529256 --- /dev/null +++ b/test/unit/srcset/test.ts @@ -0,0 +1,30 @@ +import * as assert from "assert"; +import { get_srcset_urls } from "../../../src/api/export"; + +describe("get_srcset_urls", () => { + it("should parse single entry without descriptor", () => { + const result = get_srcset_urls(""); + assert.deepEqual(result, ["assets/image/1.jpg"]); + }); + + it("should parse single entry with width descriptor", () => { + const result = get_srcset_urls(""); + assert.deepEqual(result, ["assets/image/1.jpg"]); + }); + + it("should parse single entry with density descriptor", () => { + const result = get_srcset_urls(""); + assert.deepEqual(result, ["assets/image/1.jpg"]); + }); + + it("should parse multiple entries with different descriptors", () => { + const result = get_srcset_urls(""); + assert.deepEqual(result, [ + "assets/image/1.jpg", + "assets/image/2.jpg", + "assets/image/3.jpg", + "assets/image/4.jpg", + "assets/image/5.jpg", + ]); + }); +});