165 lines
4.3 KiB
JavaScript
165 lines
4.3 KiB
JavaScript
import {
|
|
realpathSync,
|
|
statSync,
|
|
rmdirSync,
|
|
createReadStream,
|
|
promises as fs
|
|
} from 'node:fs'
|
|
import { basename, sep, join } from 'node:path'
|
|
import { tmpdir } from 'node:os'
|
|
import process from 'node:process'
|
|
|
|
import { File } from './file.js'
|
|
import { Blob } from './index.js'
|
|
|
|
const { stat, mkdtemp } = fs
|
|
let i = 0, tempDir, registry
|
|
|
|
|
|
|
|
/**
|
|
* @param {string} path filepath on the disk
|
|
* @param {string} [type] mimetype to use
|
|
*/
|
|
const blobFromSync = (path, type) => fromBlob(statSync(path), path, type)
|
|
|
|
/**
|
|
* @param {string} path filepath on the disk
|
|
* @param {string} [type] mimetype to use
|
|
* @returns {Promise<Blob>}
|
|
*/
|
|
const blobFrom = (path, type) => stat(path).then(stat => fromBlob(stat, path, type))
|
|
|
|
/**
|
|
* @param {string} path filepath on the disk
|
|
* @param {string} [type] mimetype to use
|
|
* @returns {Promise<File>}
|
|
*/
|
|
const fileFrom = (path, type) => stat(path).then(stat => fromFile(stat, path, type))
|
|
|
|
/**
|
|
* @param {string} path filepath on the disk
|
|
* @param {string} [type] mimetype to use
|
|
*/
|
|
const fileFromSync = (path, type) => fromFile(statSync(path), path, type)
|
|
|
|
// @ts-ignore
|
|
const fromBlob = (stat, path, type = '') => new Blob([new BlobDataItem({
|
|
path,
|
|
size: stat.size,
|
|
lastModified: stat.mtimeMs,
|
|
start: 0
|
|
})], { type })
|
|
|
|
// @ts-ignore
|
|
const fromFile = (stat, path, type = '') => new File([new BlobDataItem({
|
|
path,
|
|
size: stat.size,
|
|
lastModified: stat.mtimeMs,
|
|
start: 0
|
|
})], basename(path), { type, lastModified: stat.mtimeMs })
|
|
|
|
/**
|
|
* Creates a temporary blob backed by the filesystem.
|
|
* NOTE: requires node.js v14 or higher to use FinalizationRegistry
|
|
*
|
|
* @param {*} data Same as fs.writeFile data
|
|
* @param {BlobPropertyBag & {signal?: AbortSignal}} options
|
|
* @param {AbortSignal} [signal] in case you wish to cancel the write operation
|
|
* @returns {Promise<Blob>}
|
|
*/
|
|
const createTemporaryBlob = async (data, {signal, type} = {}) => {
|
|
registry = registry || new FinalizationRegistry(fs.unlink)
|
|
tempDir = tempDir || await mkdtemp(realpathSync(tmpdir()) + sep)
|
|
const id = `${i++}`
|
|
const destination = join(tempDir, id)
|
|
if (data instanceof ArrayBuffer) data = new Uint8Array(data)
|
|
await fs.writeFile(destination, data, { signal })
|
|
const blob = await blobFrom(destination, type)
|
|
registry.register(blob, destination)
|
|
return blob
|
|
}
|
|
|
|
/**
|
|
* Creates a temporary File backed by the filesystem.
|
|
* Pretty much the same as constructing a new File(data, name, options)
|
|
*
|
|
* NOTE: requires node.js v14 or higher to use FinalizationRegistry
|
|
* @param {*} data
|
|
* @param {string} name
|
|
* @param {FilePropertyBag & {signal?: AbortSignal}} opts
|
|
* @returns {Promise<File>}
|
|
*/
|
|
const createTemporaryFile = async (data, name, opts) => {
|
|
const blob = await createTemporaryBlob(data)
|
|
return new File([blob], name, opts)
|
|
}
|
|
|
|
/**
|
|
* This is a blob backed up by a file on the disk
|
|
* with minium requirement. Its wrapped around a Blob as a blobPart
|
|
* so you have no direct access to this.
|
|
*
|
|
* @private
|
|
*/
|
|
class BlobDataItem {
|
|
#path
|
|
#start
|
|
|
|
constructor (options) {
|
|
this.#path = options.path
|
|
this.#start = options.start
|
|
this.size = options.size
|
|
this.lastModified = options.lastModified
|
|
this.originalSize = options.originalSize === undefined
|
|
? options.size
|
|
: options.originalSize
|
|
}
|
|
|
|
/**
|
|
* Slicing arguments is first validated and formatted
|
|
* to not be out of range by Blob.prototype.slice
|
|
*/
|
|
slice (start, end) {
|
|
return new BlobDataItem({
|
|
path: this.#path,
|
|
lastModified: this.lastModified,
|
|
originalSize: this.originalSize,
|
|
size: end - start,
|
|
start: this.#start + start
|
|
})
|
|
}
|
|
|
|
async * stream () {
|
|
const { mtimeMs, size } = await stat(this.#path)
|
|
|
|
if (mtimeMs > this.lastModified || this.originalSize !== size) {
|
|
throw new DOMException('The requested file could not be read, typically due to permission problems that have occurred after a reference to a file was acquired.', 'NotReadableError')
|
|
}
|
|
|
|
yield * createReadStream(this.#path, {
|
|
start: this.#start,
|
|
end: this.#start + this.size - 1
|
|
})
|
|
}
|
|
|
|
get [Symbol.toStringTag] () {
|
|
return 'Blob'
|
|
}
|
|
}
|
|
|
|
process.once('exit', () => {
|
|
tempDir && rmdirSync(tempDir, { recursive: true })
|
|
})
|
|
|
|
export default blobFromSync
|
|
export {
|
|
Blob,
|
|
blobFrom,
|
|
blobFromSync,
|
|
createTemporaryBlob,
|
|
File,
|
|
fileFrom,
|
|
fileFromSync,
|
|
createTemporaryFile
|
|
} |