Skip to content

Commit fa77ceb

Browse files
marvinjudeLekoArtspieh
authored
fix(gatsby-source-filesystem): Replace special filename characters (#34249)
Co-authored-by: LekoArts <[email protected]> Co-authored-by: marvinjude <[email protected]> Co-authored-by: LekoArts <[email protected]> Co-authored-by: Michal Piechowiak <[email protected]>
1 parent 9b9419c commit fa77ceb

File tree

9 files changed

+118
-20
lines changed

9 files changed

+118
-20
lines changed

packages/gatsby-core-utils/src/__tests__/fetch-remote-file.js

+29-2
Original file line numberDiff line numberDiff line change
@@ -124,14 +124,28 @@ const server = setupServer(
124124
ctx.body(content)
125125
)
126126
}),
127+
rest.get(
128+
`http://external.com/invalid:dog*name.jpg`,
129+
async (req, res, ctx) => {
130+
const { content, contentLength } = await getFileContent(
131+
path.join(__dirname, `./fixtures/dog-thumbnail.jpg`),
132+
req
133+
)
134+
135+
return res(
136+
ctx.set(`Content-Type`, `image/jpg`),
137+
ctx.set(`Content-Length`, contentLength),
138+
ctx.status(200),
139+
ctx.body(content)
140+
)
141+
}
142+
),
127143
rest.get(`http://external.com/dog-304.jpg`, async (req, res, ctx) => {
128144
const { content, contentLength } = await getFileContent(
129145
path.join(__dirname, `./fixtures/dog-thumbnail.jpg`),
130146
req
131147
)
132148

133-
// console.log(req.headers)
134-
135149
return res(
136150
ctx.set(`Content-Type`, `image/jpg`),
137151
ctx.set(`Content-Length`, contentLength),
@@ -301,6 +315,19 @@ describe(`fetch-remote-file`, () => {
301315
expect(gotStream).toBeCalledTimes(1)
302316
})
303317

318+
it(`downloads and create a jpg file that has invalid characters`, async () => {
319+
const filePath = await fetchRemoteFile({
320+
url: `http://external.com/invalid:dog*name.jpg`,
321+
cache,
322+
})
323+
324+
expect(path.basename(filePath, `.js`)).toContain(`invalid-dog-name`)
325+
expect(getFileSize(filePath)).resolves.toBe(
326+
await getFileSize(path.join(__dirname, `./fixtures/dog-thumbnail.jpg`))
327+
)
328+
expect(gotStream).toBeCalledTimes(1)
329+
})
330+
304331
it(`doesn't retry when no content-length is given`, async () => {
305332
const filePath = await fetchRemoteFile({
306333
url: `http://external.com/logo-gzip.svg?attempts=1&maxBytes=300&contentLength=false`,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import * as path from "path"
2+
import { createFilePath, createFileHash } from "../filename-utils"
3+
4+
describe(`createFilePath`, () => {
5+
it(`handles one instance of incorrect char`, () => {
6+
const filename = `some:invalid`
7+
const assert = path.join(
8+
`dir`,
9+
`some-invalid-${createFileHash(filename)}.png`
10+
)
11+
expect(createFilePath(`dir`, filename, `.png`)).toBe(assert)
12+
})
13+
it(`handles filename without forbibben chars correctly`, () => {
14+
const filename = `some-valid`
15+
const assert = path.join(`dir`, `${filename}.png`)
16+
expect(createFilePath(`dir`, filename, `.png`)).toBe(assert)
17+
})
18+
it(`handles all instances of incorrect chars`, () => {
19+
const filename = `a:b*c?d"e<f>`
20+
const assert = path.join(
21+
`dir`,
22+
`a-b-c-d-e-f--${createFileHash(filename)}.png`
23+
)
24+
expect(createFilePath(`dir`, filename, `.png`)).toBe(assert)
25+
})
26+
it(`creates different files for possible duplicates`, () => {
27+
const filename1 = `some:file`
28+
const filename2 = `some*file`
29+
const assert1 = path.join(
30+
`dir`,
31+
`some-file-${createFileHash(filename1)}.png`
32+
)
33+
const assert2 = path.join(
34+
`dir`,
35+
`some-file-${createFileHash(filename2)}.png`
36+
)
37+
expect(createFilePath(`dir`, filename1, `.png`)).toBe(assert1)
38+
expect(createFilePath(`dir`, filename2, `.png`)).toBe(assert2)
39+
})
40+
})

packages/gatsby-core-utils/src/filename-utils.ts

+23-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from "path"
2+
import crypto from "crypto"
23
import Url from "url"
34

45
/**
@@ -31,14 +32,35 @@ export function getRemoteFileName(url: string): string {
3132
return getParsedPath(url).name
3233
}
3334

35+
export function createFileHash(input: string, length: number = 8): string {
36+
return crypto
37+
.createHash(`sha1`)
38+
.update(input)
39+
.digest(`hex`)
40+
.substring(0, length)
41+
}
42+
43+
const filenamePurgeRegex = /:|\/|\*|\?|"|<|>|\||\\/g
44+
3445
/**
3546
* createFilePath
3647
* --
48+
* Gets full file path while replacing forbidden characters with a `-`
3749
*/
3850
export function createFilePath(
3951
directory: string,
4052
filename: string,
4153
ext: string
4254
): string {
43-
return path.join(directory, `${filename}${ext}`)
55+
const purgedFileName = filename.replace(filenamePurgeRegex, `-`)
56+
const shouldAddHash = purgedFileName !== filename
57+
58+
if (shouldAddHash) {
59+
return path.join(
60+
directory,
61+
`${purgedFileName}-${createFileHash(filename)}${ext}`
62+
)
63+
} else {
64+
return path.join(directory, `${filename}${ext}`)
65+
}
4466
}

packages/gatsby-core-utils/src/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ export * from "./site-metadata"
1616
export * from "./page-data"
1717
export * from "./page-html"
1818
export { listPlugins } from "./list-plugins"
19+
export { createFilePath } from "./filename-utils"

packages/gatsby-source-filesystem/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,8 @@ When building source plugins for remote data sources such as headless CMSs, thei
162162

163163
The `createRemoteFileNode` helper makes it easy to download remote files and add them to your site's GraphQL schema.
164164

165+
While downloading the assets, special characters (regex: `/:|\/|\*|\?|"|<|>|\||\\/g`) in filenames are replaced with a hyphen "-". When special characters are found a file hash is added to keep files unique e.g `a:file.jpg` becomes `a-file-73hd.jpg` (as otherwise `a:file.jpg` and `a*file.jpg` would overwrite themselves).
166+
165167
```javascript
166168
createRemoteFileNode({
167169
// The source url of the remote file

packages/gatsby-source-filesystem/src/__tests__/create-file-node-from-buffer.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ describe(`create-file-node-from-buffer`, () => {
8383
})
8484

8585
const buffer = createMockBuffer(`buffer-content`)
86-
await setup({ buffer })
86+
await setup({ buffer, hash: `some-hash` })
8787

8888
expect(ensureDir).toBeCalledTimes(1)
8989
expect(bufferEq(buffer, output)).toBe(true)
@@ -125,6 +125,8 @@ describe(`create-file-node-from-buffer`, () => {
125125
expect(() => {
126126
createFileNodeFromBuffer({
127127
...defaultArgs,
128+
buffer: createMockBuffer(`some binary content`),
129+
hash: `some-hash`,
128130
createNode: undefined,
129131
})
130132
}).toThrowErrorMatchingInlineSnapshot(
@@ -136,6 +138,8 @@ describe(`create-file-node-from-buffer`, () => {
136138
expect(() => {
137139
createFileNodeFromBuffer({
138140
...defaultArgs,
141+
buffer: createMockBuffer(`some binary content`),
142+
hash: `some-hash`,
139143
createNodeId: undefined,
140144
})
141145
}).toThrowErrorMatchingInlineSnapshot(
@@ -147,6 +151,8 @@ describe(`create-file-node-from-buffer`, () => {
147151
expect(() => {
148152
createFileNodeFromBuffer({
149153
...defaultArgs,
154+
buffer: createMockBuffer(`some binary content`),
155+
hash: `some-hash`,
150156
cache: undefined,
151157
getCache: undefined,
152158
})
@@ -159,6 +165,8 @@ describe(`create-file-node-from-buffer`, () => {
159165
expect(() => {
160166
createFileNodeFromBuffer({
161167
...defaultArgs,
168+
buffer: createMockBuffer(`some binary content`),
169+
hash: `some-hash`,
162170
getCache: () => createMockCache(),
163171
})
164172
}).not.toThrow()
@@ -168,6 +176,8 @@ describe(`create-file-node-from-buffer`, () => {
168176
expect(() => {
169177
createFileNodeFromBuffer({
170178
...defaultArgs,
179+
buffer: createMockBuffer(`some binary content`),
180+
hash: `some-hash`,
171181
cache: createMockCache(),
172182
})
173183
}).not.toThrow()

packages/gatsby-source-filesystem/src/create-file-node-from-buffer.js

+1-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ const path = require(`path`)
33
const fileType = require(`file-type`)
44

55
const { createFileNode } = require(`./create-file-node`)
6-
const { createFilePath } = require(`./utils`)
7-
const { createContentDigest } = require(`gatsby-core-utils`)
6+
const { createContentDigest, createFilePath } = require(`gatsby-core-utils`)
87
const cacheId = hash => `create-file-node-from-buffer-${hash}`
98

109
/********************

packages/gatsby-source-filesystem/src/create-remote-file-node.js

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
const fs = require(`fs-extra`)
2-
const { createContentDigest, fetchRemoteFile } = require(`gatsby-core-utils`)
2+
const {
3+
createContentDigest,
4+
fetchRemoteFile,
5+
createFilePath,
6+
} = require(`gatsby-core-utils`)
37
const path = require(`path`)
48
const { isWebUri } = require(`valid-url`)
59
const Queue = require(`fastq`)
610
const { createFileNode } = require(`./create-file-node`)
7-
const { getRemoteFileExtension, createFilePath } = require(`./utils`)
11+
const { getRemoteFileExtension } = require(`./utils`)
812

913
let showFlagWarning = !!process.env.GATSBY_EXPERIMENTAL_REMOTE_FILE_PLACEHOLDER
1014

packages/gatsby-source-filesystem/src/utils.js

+5-12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
const path = require(`path`)
22
const Url = require(`url`)
3+
const { createFilePath } = require(`gatsby-core-utils`)
34

45
/**
56
* getParsedPath
@@ -40,15 +41,7 @@ export function getRemoteFileName(url) {
4041
return getParsedPath(url).name
4142
}
4243

43-
/**
44-
* createFilePath
45-
* --
46-
*
47-
* @param {String} directory
48-
* @param {String} filename
49-
* @param {String} ext
50-
* @return {String}
51-
*/
52-
export function createFilePath(directory, filename, ext) {
53-
return path.join(directory, `${filename}${ext}`)
54-
}
44+
// createFilePath should be imported from `gatsby-core-utils`
45+
// but some plugins already do import it from `gatsby-source-filesystem/utils`
46+
// so just keeping re-export here for backward compatibility
47+
export { createFilePath }

0 commit comments

Comments
 (0)