Skip to content

Commit cd1cf8d

Browse files
committed
feat: add setFile
1 parent 6b15fd5 commit cd1cf8d

File tree

5 files changed

+131
-15
lines changed

5 files changed

+131
-15
lines changed

.nvmrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
18.16

package-lock.json

Lines changed: 46 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
"version": "1.4.0",
44
"description": "A JavaScript client for the Netlify Blob Store",
55
"type": "module",
6-
"main": "./dist/main.js",
7-
"exports": "./dist/main.js",
6+
"main": "./dist/src/main.js",
7+
"exports": "./dist/src/main.js",
88
"files": [
9-
"dist/**/*.js",
10-
"dist/**/*.d.ts"
9+
"dist/src/**/*.js",
10+
"dist/src/**/*.d.ts",
11+
"!dist/src/**/*.test.d.ts"
1112
],
1213
"scripts": {
1314
"build": "run-s build:*",
1415
"build:check": "tsc",
15-
"build:transpile": "esbuild src/main.ts --bundle --outdir=dist --platform=node --format=esm",
16+
"build:transpile": "esbuild src/main.ts --bundle --outdir=dist/src --platform=node --format=esm",
17+
"dev": "esbuild src/main.ts --bundle --outdir=dist/src --platform=node --format=esm --watch",
1618
"prepare": "husky install node_modules/@netlify/eslint-config-node/.husky/",
1719
"prepublishOnly": "npm ci && npm test",
1820
"prepack": "npm run build",
@@ -29,8 +31,7 @@
2931
"test:ci": "run-s build test:ci:*",
3032
"test:dev:vitest": "vitest run",
3133
"test:dev:vitest:watch": "vitest watch",
32-
"test:ci:vitest": "vitest run",
33-
"watch": "tsc --watch"
34+
"test:ci:vitest": "vitest run"
3435
},
3536
"config": {
3637
"eslint": "--ignore-path .gitignore --cache --format=codeframe --max-warnings=0 \"{src,scripts,.github}/**/*.{js,ts,md,html}\" \"*.{js,ts,md,html}\"",
@@ -54,6 +55,7 @@
5455
"husky": "^8.0.0",
5556
"node-fetch": "^3.3.1",
5657
"semver": "^7.5.3",
58+
"tmp-promise": "^3.0.3",
5759
"typescript": "^5.0.0",
5860
"vitest": "^0.33.0"
5961
},

src/main.test.ts

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
import { version as nodeVersion } from 'process'
1+
import { writeFile } from 'node:fs/promises'
2+
import { version as nodeVersion } from 'node:process'
23

34
import semver from 'semver'
5+
import tmp from 'tmp-promise'
46
import { describe, test, expect, beforeAll } from 'vitest'
57

68
import { streamToString } from '../test/util.js'
@@ -310,6 +312,50 @@ describe('set', () => {
310312
await blobs.set(key, value, { ttl })
311313
})
312314

315+
test('Accepts a file', async () => {
316+
expect.assertions(5)
317+
318+
const fileContents = 'Hello from a file'
319+
const fetcher = async (...args: Parameters<typeof globalThis.fetch>) => {
320+
const [url, options] = args
321+
const headers = options?.headers as Record<string, string>
322+
323+
expect(options?.method).toBe('put')
324+
325+
if (url === `https://api.netlify.com/api/v1/sites/${siteID}/blobs/${key}?context=production`) {
326+
const data = JSON.stringify({ url: signedURL })
327+
328+
expect(headers.authorization).toBe(`Bearer ${apiToken}`)
329+
330+
return new Response(data)
331+
}
332+
333+
if (url === signedURL) {
334+
expect(await streamToString(options?.body as unknown as NodeJS.ReadableStream)).toBe(fileContents)
335+
expect(headers['cache-control']).toBe('max-age=0, stale-while-revalidate=60')
336+
337+
return new Response(value)
338+
}
339+
340+
throw new Error(`Unexpected fetch call: ${url}`)
341+
}
342+
343+
const { cleanup, path } = await tmp.file()
344+
345+
await writeFile(path, fileContents)
346+
347+
const blobs = new Blobs({
348+
authentication: {
349+
token: apiToken,
350+
},
351+
fetcher,
352+
siteID,
353+
})
354+
355+
await blobs.setFile(key, path)
356+
await cleanup()
357+
})
358+
313359
test('Throws when the API returns a non-200 status code', async () => {
314360
const fetcher = async (...args: Parameters<typeof globalThis.fetch>) => {
315361
const [url, options] = args

src/main.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import { createReadStream } from 'node:fs'
2+
import { stat } from 'node:fs/promises'
3+
import { Readable } from 'node:stream'
4+
15
interface APICredentials {
26
apiURL?: string
37
token: string
@@ -127,7 +131,19 @@ export class Blobs {
127131
headers['cache-control'] = 'max-age=0, stale-while-revalidate=60'
128132
}
129133

130-
const res = await this.fetcher(url, { body, headers, method })
134+
const options: RequestInit = {
135+
body,
136+
headers,
137+
method,
138+
}
139+
140+
if (body instanceof ReadableStream) {
141+
// @ts-expect-error Part of the spec, but not typed:
142+
// https://fetch.spec.whatwg.org/#enumdef-requestduplex
143+
options.duplex = 'half'
144+
}
145+
146+
const res = await this.fetcher(url, options)
131147

132148
if (res.status === 404 && method === HTTPMethod.Get) {
133149
return null
@@ -200,6 +216,17 @@ export class Blobs {
200216
await this.makeStoreRequest(key, HTTPMethod.Put, headers, data)
201217
}
202218

219+
async setFile(key: string, path: string, { ttl }: SetOptions = {}) {
220+
const { size } = await stat(path)
221+
const file = Readable.toWeb(createReadStream(path))
222+
const headers = {
223+
...Blobs.getTTLHeaders(ttl),
224+
'content-length': size.toString(),
225+
}
226+
227+
await this.makeStoreRequest(key, HTTPMethod.Put, headers, file as ReadableStream)
228+
}
229+
203230
async setJSON(key: string, data: unknown, { ttl }: SetOptions = {}) {
204231
const payload = JSON.stringify(data)
205232
const headers = {

0 commit comments

Comments
 (0)