444 lines
9.7 KiB
JavaScript
444 lines
9.7 KiB
JavaScript
|
'use strict'
|
||
|
const { Minipass } = require('minipass')
|
||
|
const EE = require('events').EventEmitter
|
||
|
const fs = require('fs')
|
||
|
|
||
|
const writev = fs.writev
|
||
|
|
||
|
const _autoClose = Symbol('_autoClose')
|
||
|
const _close = Symbol('_close')
|
||
|
const _ended = Symbol('_ended')
|
||
|
const _fd = Symbol('_fd')
|
||
|
const _finished = Symbol('_finished')
|
||
|
const _flags = Symbol('_flags')
|
||
|
const _flush = Symbol('_flush')
|
||
|
const _handleChunk = Symbol('_handleChunk')
|
||
|
const _makeBuf = Symbol('_makeBuf')
|
||
|
const _mode = Symbol('_mode')
|
||
|
const _needDrain = Symbol('_needDrain')
|
||
|
const _onerror = Symbol('_onerror')
|
||
|
const _onopen = Symbol('_onopen')
|
||
|
const _onread = Symbol('_onread')
|
||
|
const _onwrite = Symbol('_onwrite')
|
||
|
const _open = Symbol('_open')
|
||
|
const _path = Symbol('_path')
|
||
|
const _pos = Symbol('_pos')
|
||
|
const _queue = Symbol('_queue')
|
||
|
const _read = Symbol('_read')
|
||
|
const _readSize = Symbol('_readSize')
|
||
|
const _reading = Symbol('_reading')
|
||
|
const _remain = Symbol('_remain')
|
||
|
const _size = Symbol('_size')
|
||
|
const _write = Symbol('_write')
|
||
|
const _writing = Symbol('_writing')
|
||
|
const _defaultFlag = Symbol('_defaultFlag')
|
||
|
const _errored = Symbol('_errored')
|
||
|
|
||
|
class ReadStream extends Minipass {
|
||
|
constructor (path, opt) {
|
||
|
opt = opt || {}
|
||
|
super(opt)
|
||
|
|
||
|
this.readable = true
|
||
|
this.writable = false
|
||
|
|
||
|
if (typeof path !== 'string') {
|
||
|
throw new TypeError('path must be a string')
|
||
|
}
|
||
|
|
||
|
this[_errored] = false
|
||
|
this[_fd] = typeof opt.fd === 'number' ? opt.fd : null
|
||
|
this[_path] = path
|
||
|
this[_readSize] = opt.readSize || 16 * 1024 * 1024
|
||
|
this[_reading] = false
|
||
|
this[_size] = typeof opt.size === 'number' ? opt.size : Infinity
|
||
|
this[_remain] = this[_size]
|
||
|
this[_autoClose] = typeof opt.autoClose === 'boolean' ?
|
||
|
opt.autoClose : true
|
||
|
|
||
|
if (typeof this[_fd] === 'number') {
|
||
|
this[_read]()
|
||
|
} else {
|
||
|
this[_open]()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get fd () {
|
||
|
return this[_fd]
|
||
|
}
|
||
|
|
||
|
get path () {
|
||
|
return this[_path]
|
||
|
}
|
||
|
|
||
|
write () {
|
||
|
throw new TypeError('this is a readable stream')
|
||
|
}
|
||
|
|
||
|
end () {
|
||
|
throw new TypeError('this is a readable stream')
|
||
|
}
|
||
|
|
||
|
[_open] () {
|
||
|
fs.open(this[_path], 'r', (er, fd) => this[_onopen](er, fd))
|
||
|
}
|
||
|
|
||
|
[_onopen] (er, fd) {
|
||
|
if (er) {
|
||
|
this[_onerror](er)
|
||
|
} else {
|
||
|
this[_fd] = fd
|
||
|
this.emit('open', fd)
|
||
|
this[_read]()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_makeBuf] () {
|
||
|
return Buffer.allocUnsafe(Math.min(this[_readSize], this[_remain]))
|
||
|
}
|
||
|
|
||
|
[_read] () {
|
||
|
if (!this[_reading]) {
|
||
|
this[_reading] = true
|
||
|
const buf = this[_makeBuf]()
|
||
|
/* istanbul ignore if */
|
||
|
if (buf.length === 0) {
|
||
|
return process.nextTick(() => this[_onread](null, 0, buf))
|
||
|
}
|
||
|
fs.read(this[_fd], buf, 0, buf.length, null, (er, br, b) =>
|
||
|
this[_onread](er, br, b))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_onread] (er, br, buf) {
|
||
|
this[_reading] = false
|
||
|
if (er) {
|
||
|
this[_onerror](er)
|
||
|
} else if (this[_handleChunk](br, buf)) {
|
||
|
this[_read]()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_close] () {
|
||
|
if (this[_autoClose] && typeof this[_fd] === 'number') {
|
||
|
const fd = this[_fd]
|
||
|
this[_fd] = null
|
||
|
fs.close(fd, er => er ? this.emit('error', er) : this.emit('close'))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_onerror] (er) {
|
||
|
this[_reading] = true
|
||
|
this[_close]()
|
||
|
this.emit('error', er)
|
||
|
}
|
||
|
|
||
|
[_handleChunk] (br, buf) {
|
||
|
let ret = false
|
||
|
// no effect if infinite
|
||
|
this[_remain] -= br
|
||
|
if (br > 0) {
|
||
|
ret = super.write(br < buf.length ? buf.slice(0, br) : buf)
|
||
|
}
|
||
|
|
||
|
if (br === 0 || this[_remain] <= 0) {
|
||
|
ret = false
|
||
|
this[_close]()
|
||
|
super.end()
|
||
|
}
|
||
|
|
||
|
return ret
|
||
|
}
|
||
|
|
||
|
emit (ev, data) {
|
||
|
switch (ev) {
|
||
|
case 'prefinish':
|
||
|
case 'finish':
|
||
|
break
|
||
|
|
||
|
case 'drain':
|
||
|
if (typeof this[_fd] === 'number') {
|
||
|
this[_read]()
|
||
|
}
|
||
|
break
|
||
|
|
||
|
case 'error':
|
||
|
if (this[_errored]) {
|
||
|
return
|
||
|
}
|
||
|
this[_errored] = true
|
||
|
return super.emit(ev, data)
|
||
|
|
||
|
default:
|
||
|
return super.emit(ev, data)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ReadStreamSync extends ReadStream {
|
||
|
[_open] () {
|
||
|
let threw = true
|
||
|
try {
|
||
|
this[_onopen](null, fs.openSync(this[_path], 'r'))
|
||
|
threw = false
|
||
|
} finally {
|
||
|
if (threw) {
|
||
|
this[_close]()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_read] () {
|
||
|
let threw = true
|
||
|
try {
|
||
|
if (!this[_reading]) {
|
||
|
this[_reading] = true
|
||
|
do {
|
||
|
const buf = this[_makeBuf]()
|
||
|
/* istanbul ignore next */
|
||
|
const br = buf.length === 0 ? 0
|
||
|
: fs.readSync(this[_fd], buf, 0, buf.length, null)
|
||
|
if (!this[_handleChunk](br, buf)) {
|
||
|
break
|
||
|
}
|
||
|
} while (true)
|
||
|
this[_reading] = false
|
||
|
}
|
||
|
threw = false
|
||
|
} finally {
|
||
|
if (threw) {
|
||
|
this[_close]()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_close] () {
|
||
|
if (this[_autoClose] && typeof this[_fd] === 'number') {
|
||
|
const fd = this[_fd]
|
||
|
this[_fd] = null
|
||
|
fs.closeSync(fd)
|
||
|
this.emit('close')
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class WriteStream extends EE {
|
||
|
constructor (path, opt) {
|
||
|
opt = opt || {}
|
||
|
super(opt)
|
||
|
this.readable = false
|
||
|
this.writable = true
|
||
|
this[_errored] = false
|
||
|
this[_writing] = false
|
||
|
this[_ended] = false
|
||
|
this[_needDrain] = false
|
||
|
this[_queue] = []
|
||
|
this[_path] = path
|
||
|
this[_fd] = typeof opt.fd === 'number' ? opt.fd : null
|
||
|
this[_mode] = opt.mode === undefined ? 0o666 : opt.mode
|
||
|
this[_pos] = typeof opt.start === 'number' ? opt.start : null
|
||
|
this[_autoClose] = typeof opt.autoClose === 'boolean' ?
|
||
|
opt.autoClose : true
|
||
|
|
||
|
// truncating makes no sense when writing into the middle
|
||
|
const defaultFlag = this[_pos] !== null ? 'r+' : 'w'
|
||
|
this[_defaultFlag] = opt.flags === undefined
|
||
|
this[_flags] = this[_defaultFlag] ? defaultFlag : opt.flags
|
||
|
|
||
|
if (this[_fd] === null) {
|
||
|
this[_open]()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
emit (ev, data) {
|
||
|
if (ev === 'error') {
|
||
|
if (this[_errored]) {
|
||
|
return
|
||
|
}
|
||
|
this[_errored] = true
|
||
|
}
|
||
|
return super.emit(ev, data)
|
||
|
}
|
||
|
|
||
|
get fd () {
|
||
|
return this[_fd]
|
||
|
}
|
||
|
|
||
|
get path () {
|
||
|
return this[_path]
|
||
|
}
|
||
|
|
||
|
[_onerror] (er) {
|
||
|
this[_close]()
|
||
|
this[_writing] = true
|
||
|
this.emit('error', er)
|
||
|
}
|
||
|
|
||
|
[_open] () {
|
||
|
fs.open(this[_path], this[_flags], this[_mode],
|
||
|
(er, fd) => this[_onopen](er, fd))
|
||
|
}
|
||
|
|
||
|
[_onopen] (er, fd) {
|
||
|
if (this[_defaultFlag] &&
|
||
|
this[_flags] === 'r+' &&
|
||
|
er && er.code === 'ENOENT') {
|
||
|
this[_flags] = 'w'
|
||
|
this[_open]()
|
||
|
} else if (er) {
|
||
|
this[_onerror](er)
|
||
|
} else {
|
||
|
this[_fd] = fd
|
||
|
this.emit('open', fd)
|
||
|
if (!this[_writing]) {
|
||
|
this[_flush]()
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
end (buf, enc) {
|
||
|
if (buf) {
|
||
|
this.write(buf, enc)
|
||
|
}
|
||
|
|
||
|
this[_ended] = true
|
||
|
|
||
|
// synthetic after-write logic, where drain/finish live
|
||
|
if (!this[_writing] && !this[_queue].length &&
|
||
|
typeof this[_fd] === 'number') {
|
||
|
this[_onwrite](null, 0)
|
||
|
}
|
||
|
return this
|
||
|
}
|
||
|
|
||
|
write (buf, enc) {
|
||
|
if (typeof buf === 'string') {
|
||
|
buf = Buffer.from(buf, enc)
|
||
|
}
|
||
|
|
||
|
if (this[_ended]) {
|
||
|
this.emit('error', new Error('write() after end()'))
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
if (this[_fd] === null || this[_writing] || this[_queue].length) {
|
||
|
this[_queue].push(buf)
|
||
|
this[_needDrain] = true
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
this[_writing] = true
|
||
|
this[_write](buf)
|
||
|
return true
|
||
|
}
|
||
|
|
||
|
[_write] (buf) {
|
||
|
fs.write(this[_fd], buf, 0, buf.length, this[_pos], (er, bw) =>
|
||
|
this[_onwrite](er, bw))
|
||
|
}
|
||
|
|
||
|
[_onwrite] (er, bw) {
|
||
|
if (er) {
|
||
|
this[_onerror](er)
|
||
|
} else {
|
||
|
if (this[_pos] !== null) {
|
||
|
this[_pos] += bw
|
||
|
}
|
||
|
if (this[_queue].length) {
|
||
|
this[_flush]()
|
||
|
} else {
|
||
|
this[_writing] = false
|
||
|
|
||
|
if (this[_ended] && !this[_finished]) {
|
||
|
this[_finished] = true
|
||
|
this[_close]()
|
||
|
this.emit('finish')
|
||
|
} else if (this[_needDrain]) {
|
||
|
this[_needDrain] = false
|
||
|
this.emit('drain')
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_flush] () {
|
||
|
if (this[_queue].length === 0) {
|
||
|
if (this[_ended]) {
|
||
|
this[_onwrite](null, 0)
|
||
|
}
|
||
|
} else if (this[_queue].length === 1) {
|
||
|
this[_write](this[_queue].pop())
|
||
|
} else {
|
||
|
const iovec = this[_queue]
|
||
|
this[_queue] = []
|
||
|
writev(this[_fd], iovec, this[_pos],
|
||
|
(er, bw) => this[_onwrite](er, bw))
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_close] () {
|
||
|
if (this[_autoClose] && typeof this[_fd] === 'number') {
|
||
|
const fd = this[_fd]
|
||
|
this[_fd] = null
|
||
|
fs.close(fd, er => er ? this.emit('error', er) : this.emit('close'))
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class WriteStreamSync extends WriteStream {
|
||
|
[_open] () {
|
||
|
let fd
|
||
|
// only wrap in a try{} block if we know we'll retry, to avoid
|
||
|
// the rethrow obscuring the error's source frame in most cases.
|
||
|
if (this[_defaultFlag] && this[_flags] === 'r+') {
|
||
|
try {
|
||
|
fd = fs.openSync(this[_path], this[_flags], this[_mode])
|
||
|
} catch (er) {
|
||
|
if (er.code === 'ENOENT') {
|
||
|
this[_flags] = 'w'
|
||
|
return this[_open]()
|
||
|
} else {
|
||
|
throw er
|
||
|
}
|
||
|
}
|
||
|
} else {
|
||
|
fd = fs.openSync(this[_path], this[_flags], this[_mode])
|
||
|
}
|
||
|
|
||
|
this[_onopen](null, fd)
|
||
|
}
|
||
|
|
||
|
[_close] () {
|
||
|
if (this[_autoClose] && typeof this[_fd] === 'number') {
|
||
|
const fd = this[_fd]
|
||
|
this[_fd] = null
|
||
|
fs.closeSync(fd)
|
||
|
this.emit('close')
|
||
|
}
|
||
|
}
|
||
|
|
||
|
[_write] (buf) {
|
||
|
// throw the original, but try to close if it fails
|
||
|
let threw = true
|
||
|
try {
|
||
|
this[_onwrite](null,
|
||
|
fs.writeSync(this[_fd], buf, 0, buf.length, this[_pos]))
|
||
|
threw = false
|
||
|
} finally {
|
||
|
if (threw) {
|
||
|
try {
|
||
|
this[_close]()
|
||
|
} catch {
|
||
|
// ok error
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
exports.ReadStream = ReadStream
|
||
|
exports.ReadStreamSync = ReadStreamSync
|
||
|
|
||
|
exports.WriteStream = WriteStream
|
||
|
exports.WriteStreamSync = WriteStreamSync
|