Skip to content

Commit 20ee08f

Browse files
authored
feat: new way of creating temporary files backed up by fs (#137)
* feat: new way of creating temporary files backed up by fs * add readme
1 parent f077471 commit 20ee08f

File tree

3 files changed

+132
-5
lines changed

3 files changed

+132
-5
lines changed

README.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,34 @@ console.log(blob.size) // ~4 GiB
8787

8888
`blobFrom|blobFromSync|fileFrom|fileFromSync(path, [mimetype])`
8989

90+
### Creating a temporary file on the disk
91+
(requires [FinalizationRegistry] - node v14.6)
92+
93+
When using both `createTemporaryBlob` and `createTemporaryFile`
94+
then you will write data to the temporary folder in their respective OS.
95+
The arguments can be anything that [fsPromises.writeFile] supports. NodeJS
96+
v14.17.0+ also supports writing (async)Iterable streams and passing in a
97+
AbortSignal, so both NodeJS stream and whatwg streams are supported. When the
98+
file have been written it will return a Blob/File handle with a references to
99+
this temporary location on the disk. When you no longer have a references to
100+
this Blob/File anymore and it have been GC then it will automatically be deleted.
101+
102+
This files are also unlinked upon exiting the process.
103+
```js
104+
import { createTemporaryBlob, createTemporaryFile } from 'fetch-blob/from.js'
105+
106+
const req = new Request('https://httpbin.org/image/png')
107+
const res = await fetch(req)
108+
const type = res.headers.get('content-type')
109+
const signal = req.signal
110+
let blob = await createTemporaryBlob(res.body, { type, signal })
111+
// const file = createTemporaryBlob(res.body, 'img.png', { type, signal })
112+
blob = undefined // loosing references will delete the file from disk
113+
```
114+
115+
`createTemporaryBlob(data, { type, signal })`
116+
`createTemporaryFile(data, FileName, { type, signal, lastModified })`
117+
90118
### Creating Blobs backed up by other async sources
91119
Our Blob & File class are more generic then any other polyfills in the way that it can accept any blob look-a-like item
92120
An example of this is that our blob implementation can be constructed with parts coming from [BlobDataItem](https://github.com/node-fetch/fetch-blob/blob/8ef89adad40d255a3bbd55cf38b88597c1cd5480/from.js#L32) (aka a filepath) or from [buffer.Blob](https://nodejs.org/api/buffer.html#buffer_new_buffer_blob_sources_options), It dose not have to implement all the methods - just enough that it can be read/understood by our Blob implementation. The minium requirements is that it has `Symbol.toStringTag`, `size`, `slice()` and either a `stream()` or a `arrayBuffer()` method. If you then wrap it in our Blob or File `new Blob([blobDataItem])` then you get all of the other methods that should be implemented in a blob or file
@@ -104,3 +132,5 @@ See the [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/API/Blo
104132
[install-size-image]: https://flat.badgen.net/packagephobia/install/fetch-blob
105133
[install-size-url]: https://packagephobia.now.sh/result?p=fetch-blob
106134
[fs-blobs]: https://github.com/nodejs/node/issues/37340
135+
[fsPromises.writeFile]: https://nodejs.org/dist/latest-v18.x/docs/api/fs.html#fspromiseswritefilefile-data-options
136+
[FinalizationRegistry]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/FinalizationRegistry

from.js

Lines changed: 62 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
1-
import { statSync, createReadStream, promises as fs } from 'node:fs'
2-
import { basename } from 'node:path'
1+
import {
2+
realpathSync,
3+
statSync,
4+
rmdirSync,
5+
createReadStream,
6+
promises as fs
7+
} from 'node:fs'
8+
import { basename, sep, join } from 'node:path'
9+
import { tmpdir } from 'node:os'
10+
import process from 'node:process'
311
import DOMException from 'node-domexception'
412

513
import File from './file.js'
614
import Blob from './index.js'
715

8-
const { stat } = fs
16+
const { stat, mkdtemp } = fs
17+
let i = 0, tempDir, registry
918

1019
/**
1120
* @param {string} path filepath on the disk
@@ -49,6 +58,42 @@ const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
4958
start: 0
5059
})], basename(path), { type, lastModified: stat.mtimeMs })
5160

61+
/**
62+
* Creates a temporary blob backed by the filesystem.
63+
* NOTE: requires node.js v14 or higher to use FinalizationRegistry
64+
*
65+
* @param {*} data Same as fs.writeFile data
66+
* @param {BlobPropertyBag & {signal?: AbortSignal}} options
67+
* @param {AbortSignal} [signal] in case you wish to cancel the write operation
68+
* @returns {Promise<Blob>}
69+
*/
70+
const createTemporaryBlob = async (data, {signal, type} = {}) => {
71+
registry = registry || new FinalizationRegistry(fs.unlink)
72+
tempDir = tempDir || await mkdtemp(realpathSync(tmpdir()) + sep)
73+
const id = `${i++}`
74+
const destination = join(tempDir, id)
75+
if (data instanceof ArrayBuffer) data = new Uint8Array(data)
76+
await fs.writeFile(destination, data, { signal })
77+
const blob = await blobFrom(destination, type)
78+
registry.register(blob, destination)
79+
return blob
80+
}
81+
82+
/**
83+
* Creates a temporary File backed by the filesystem.
84+
* Pretty much the same as constructing a new File(data, name, options)
85+
*
86+
* NOTE: requires node.js v14 or higher to use FinalizationRegistry
87+
* @param {*} data
88+
* @param {string} name
89+
* @param {FilePropertyBag & {signal?: AbortSignal}} opts
90+
* @returns {Promise<File>}
91+
*/
92+
const createTemporaryFile = async (data, name, opts) => {
93+
const blob = await createTemporaryBlob(data)
94+
return new File([blob], name, opts)
95+
}
96+
5297
/**
5398
* This is a blob backed up by a file on the disk
5499
* with minium requirement. Its wrapped around a Blob as a blobPart
@@ -102,5 +147,18 @@ class BlobDataItem {
102147
}
103148
}
104149

150+
process.once('exit', () => {
151+
tempDir && rmdirSync(tempDir, { recursive: true })
152+
})
153+
105154
export default blobFromSync
106-
export { File, Blob, blobFrom, blobFromSync, fileFrom, fileFromSync }
155+
export {
156+
Blob,
157+
blobFrom,
158+
blobFromSync,
159+
createTemporaryBlob,
160+
File,
161+
fileFrom,
162+
fileFromSync,
163+
createTemporaryFile
164+
}

test/own-misc-test.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@
33

44
import fs from 'node:fs'
55
import buffer from 'node:buffer'
6-
import syncBlob, { blobFromSync, blobFrom, fileFromSync, fileFrom } from '../from.js'
6+
import syncBlob, {
7+
blobFromSync,
8+
blobFrom,
9+
fileFromSync,
10+
fileFrom,
11+
createTemporaryBlob,
12+
createTemporaryFile
13+
} from '../from.js'
714

815
const license = fs.readFileSync('./LICENSE')
916

@@ -189,6 +196,38 @@ promise_test(async () => {
189196
assert_equals(await (await fileFrom('./LICENSE')).text(), license.toString())
190197
}, 'blob part backed up by filesystem slice correctly')
191198

199+
promise_test(async () => {
200+
let blob
201+
// Can construct a temporary blob from a string
202+
blob = await createTemporaryBlob(license.toString())
203+
assert_equals(await blob.text(), license.toString())
204+
205+
// Can construct a temporary blob from a async iterator
206+
blob = await createTemporaryBlob(blob.stream())
207+
assert_equals(await blob.text(), license.toString())
208+
209+
// Can construct a temporary file from a arrayBuffer
210+
blob = await createTemporaryBlob(await blob.arrayBuffer())
211+
assert_equals(await blob.text(), license.toString())
212+
213+
// Can construct a temporary file from a arrayBufferView
214+
blob = await createTemporaryBlob(await blob.arrayBuffer().then(ab => new Uint8Array(ab)))
215+
assert_equals(await blob.text(), license.toString())
216+
217+
// Can specify a mime type
218+
blob = await createTemporaryBlob('abc', { type: 'text/plain' })
219+
assert_equals(blob.type, 'text/plain')
220+
221+
// Can create files too
222+
let file = await createTemporaryFile('abc', 'abc.txt', {
223+
type: 'text/plain',
224+
lastModified: 123
225+
})
226+
assert_equals(file.name, 'abc.txt')
227+
assert_equals(file.size, 3)
228+
assert_equals(file.lastModified, 123)
229+
}, 'creating temporary blob/file backed up by filesystem')
230+
192231
promise_test(async () => {
193232
fs.writeFileSync('temp', '')
194233
await blobFromSync('./temp').text()

0 commit comments

Comments
 (0)