Skip to content

Commit 1a9469d

Browse files
ascorbicgatsbybot
and
gatsbybot
authored
feat(gatsby-core-utils): Add file download functions (#29531)
* feat(gatsby-core-utils): Add file download functions * skiplibcheck * Export interface * Port tests * Fix type Co-authored-by: gatsbybot <[email protected]>
1 parent ee9c538 commit 1a9469d

File tree

13 files changed

+539
-201
lines changed

13 files changed

+539
-201
lines changed

packages/gatsby-core-utils/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"dependencies": {
3232
"ci-info": "2.0.0",
3333
"configstore": "^5.0.1",
34+
"file-type": "^16.2.0",
3435
"fs-extra": "^8.1.0",
3536
"node-object-hash": "^2.0.0",
3637
"proper-lockfile": "^4.1.1",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,238 @@
1+
// @ts-check
2+
import path from "path"
3+
import zlib from "zlib"
4+
import os from "os"
5+
import { rest } from "msw"
6+
import { setupServer } from "msw/node"
7+
import { Writable } from "stream"
8+
import got from "got"
9+
import { fetchRemoteFile } from "../fetch-remote-file"
10+
11+
const fs = jest.requireActual(`fs-extra`)
12+
13+
const gotStream = jest.spyOn(got, `stream`)
14+
const urlCount = new Map()
15+
16+
async function getFileSize(file) {
17+
const stat = await fs.stat(file)
18+
19+
return stat.size
20+
}
21+
22+
/**
23+
* A utility to help create file responses
24+
* - Url with attempts will use maxBytes for x amount of time until it delivers the full response
25+
* - MaxBytes indicates how much bytes we'll be sending
26+
*
27+
* @param {string} file File path on disk
28+
* @param {Object} req Is the request object from msw
29+
* @param {{ compress?: boolean}} options Options for the getFilecontent (use gzip or not)
30+
*/
31+
async function getFileContent(file, req, options = {}) {
32+
const cacheKey = req.url.origin + req.url.pathname
33+
const maxRetry = req.url.searchParams.get(`attempts`)
34+
const maxBytes = req.url.searchParams.get(`maxBytes`)
35+
const currentRetryCount = urlCount.get(cacheKey) || 0
36+
urlCount.set(cacheKey, currentRetryCount + 1)
37+
38+
let fileContentBuffer = await fs.readFile(file)
39+
if (options.compress) {
40+
fileContentBuffer = zlib.deflateSync(fileContentBuffer)
41+
}
42+
43+
const content = await new Promise(resolve => {
44+
const fileStream = fs.createReadStream(file, {
45+
end:
46+
currentRetryCount < Number(maxRetry)
47+
? Number(maxBytes)
48+
: Number.MAX_SAFE_INTEGER,
49+
})
50+
51+
const writableStream = new Writable()
52+
const result = []
53+
writableStream._write = (chunk, encoding, next) => {
54+
result.push(chunk)
55+
56+
next()
57+
}
58+
59+
writableStream.on(`finish`, () => {
60+
resolve(Buffer.concat(result))
61+
})
62+
63+
// eslint-disable-next-line no-unused-vars
64+
let stream = fileStream
65+
if (options.compress) {
66+
stream = stream.pipe(zlib.createDeflate())
67+
}
68+
69+
stream.pipe(writableStream)
70+
})
71+
72+
return {
73+
content,
74+
contentLength:
75+
req.url.searchParams.get(`contentLength`) === `false`
76+
? undefined
77+
: fileContentBuffer.length,
78+
}
79+
}
80+
81+
const server = setupServer(
82+
rest.get(`http://external.com/logo.svg`, async (req, res, ctx) => {
83+
const { content, contentLength } = await getFileContent(
84+
path.join(__dirname, `./fixtures/gatsby-logo.svg`),
85+
req
86+
)
87+
88+
return res(
89+
ctx.set(`Content-Type`, `image/svg+xml`),
90+
ctx.set(`Content-Length`, contentLength),
91+
ctx.status(200),
92+
ctx.body(content)
93+
)
94+
}),
95+
rest.get(`http://external.com/logo-gzip.svg`, async (req, res, ctx) => {
96+
const { content, contentLength } = await getFileContent(
97+
path.join(__dirname, `./fixtures/gatsby-logo.svg`),
98+
req,
99+
{
100+
compress: true,
101+
}
102+
)
103+
104+
return res(
105+
ctx.set(`Content-Type`, `image/svg+xml`),
106+
ctx.set(`content-encoding`, `gzip`),
107+
ctx.set(`Content-Length`, contentLength),
108+
ctx.status(200),
109+
ctx.body(content)
110+
)
111+
}),
112+
rest.get(`http://external.com/dog.jpg`, async (req, res, ctx) => {
113+
const { content, contentLength } = await getFileContent(
114+
path.join(__dirname, `./fixtures/dog-thumbnail.jpg`),
115+
req
116+
)
117+
118+
return res(
119+
ctx.set(`Content-Type`, `image/svg+xml`),
120+
ctx.set(`Content-Length`, contentLength),
121+
ctx.status(200),
122+
ctx.body(content)
123+
)
124+
})
125+
)
126+
127+
function createMockCache() {
128+
const tmpDir = fs.mkdtempSync(
129+
path.join(os.tmpdir(), `gatsby-source-filesystem-`)
130+
)
131+
132+
return {
133+
get: jest.fn(),
134+
set: jest.fn(),
135+
directory: tmpDir,
136+
}
137+
}
138+
139+
describe(`create-remote-file-node`, () => {
140+
let cache
141+
142+
beforeAll(() => {
143+
cache = createMockCache()
144+
// Establish requests interception layer before all tests.
145+
server.listen()
146+
})
147+
afterAll(() => {
148+
if (cache) {
149+
fs.removeSync(cache.directory)
150+
}
151+
152+
// Clean up after all tests are done, preventing this
153+
// interception layer from affecting irrelevant tests.
154+
server.close()
155+
})
156+
157+
beforeEach(() => {
158+
gotStream.mockClear()
159+
urlCount.clear()
160+
})
161+
162+
it(`downloads and create a file`, async () => {
163+
const filePath = await fetchRemoteFile({
164+
url: `http://external.com/logo.svg`,
165+
cache,
166+
})
167+
168+
expect(path.basename(filePath)).toBe(`logo.svg`)
169+
expect(gotStream).toBeCalledTimes(1)
170+
})
171+
172+
it(`downloads and create a gzip file`, async () => {
173+
const filePath = await fetchRemoteFile({
174+
url: `http://external.com/logo-gzip.svg`,
175+
cache,
176+
})
177+
178+
expect(path.basename(filePath)).toBe(`logo-gzip.svg`)
179+
expect(getFileSize(filePath)).resolves.toBe(
180+
await getFileSize(path.join(__dirname, `./fixtures/gatsby-logo.svg`))
181+
)
182+
expect(gotStream).toBeCalledTimes(1)
183+
})
184+
185+
it(`downloads and create a file`, async () => {
186+
const filePath = await fetchRemoteFile({
187+
url: `http://external.com/dog.jpg`,
188+
cache,
189+
})
190+
191+
expect(path.basename(filePath)).toBe(`dog.jpg`)
192+
expect(getFileSize(filePath)).resolves.toBe(
193+
await getFileSize(path.join(__dirname, `./fixtures/dog-thumbnail.jpg`))
194+
)
195+
expect(gotStream).toBeCalledTimes(1)
196+
})
197+
198+
it(`doesn't retry when no content-length is given`, async () => {
199+
const filePath = await fetchRemoteFile({
200+
url: `http://external.com/logo-gzip.svg?attempts=1&maxBytes=300&contentLength=false`,
201+
cache,
202+
})
203+
204+
expect(path.basename(filePath)).toBe(`logo-gzip.svg`)
205+
expect(getFileSize(filePath)).resolves.not.toBe(
206+
await getFileSize(path.join(__dirname, `./fixtures/gatsby-logo.svg`))
207+
)
208+
expect(gotStream).toBeCalledTimes(1)
209+
})
210+
211+
describe(`retries the download`, () => {
212+
it(`Retries when gzip compression file is incomplete`, async () => {
213+
const filePath = await fetchRemoteFile({
214+
url: `http://external.com/logo-gzip.svg?attempts=1&maxBytes=300`,
215+
cache,
216+
})
217+
218+
expect(path.basename(filePath)).toBe(`logo-gzip.svg`)
219+
expect(getFileSize(filePath)).resolves.toBe(
220+
await getFileSize(path.join(__dirname, `./fixtures/gatsby-logo.svg`))
221+
)
222+
expect(gotStream).toBeCalledTimes(2)
223+
})
224+
225+
it(`Retries when binary file is incomplete`, async () => {
226+
const filePath = await fetchRemoteFile({
227+
url: `http://external.com/dog.jpg?attempts=1&maxBytes=300`,
228+
cache,
229+
})
230+
231+
expect(path.basename(filePath)).toBe(`dog.jpg`)
232+
expect(getFileSize(filePath)).resolves.toBe(
233+
await getFileSize(path.join(__dirname, `./fixtures/dog-thumbnail.jpg`))
234+
)
235+
expect(gotStream).toBeCalledTimes(2)
236+
})
237+
})
238+
})
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"yo": "dog"}
Loading

0 commit comments

Comments
 (0)